Асинхронность и многопоточность во Flutter: Future, Stream и Isolates на практике

9 декабря 2025 · ? просмотров · ? мин
два телефона с логотипом flutter показывают асинхронность
...
Содержание
При разработке Flutter-приложений быстро возникает необходимость выполнять долгие операции: загрузку данных из сети, обращение к базе, работу с файлами, вычисления и т.п. Если делать это синхронно, основной поток блокируется, интерфейс «зависает», а пользователь видит «замороженный» экран. Асинхронное программирование в Dart позволяет вынести такие операции из UI-потока, не блокируя интерфейс и сохраняя приложение плавным и отзывчивым.

В данной статье мы расскажем, как во Flutter использовать ключевые инструменты асинхронности Dart — Future, async/await, Stream, а также многопоточность через Isolate — и покажем, как применять их на практике
в реальных приложениях.

Основные концепции асинхронности в Dart

В этом разделе рассматриваются базовые строительные блоки асинхронности в Dart: Future, async/await и Stream, а также то,
как они дополняют друг друга в реальных приложениях.

Future — основа асинхронности Dart

Future представляет собой отложенное вычисление, которое завершится либо успешным результатом, либо ошибкой. Это фундамент асинхронного программирования в Dart.
// Простой пример Future
Future<String> fetchUserName(int userId) {
  return Future.delayed(Duration(seconds: 2), () {
    // Имитация задержки сети
    if (userId == 1) {
      return 'Алексей Петров';
    } else {
      throw Exception('Пользователь не найден');
    }
  });
}
// Использование
void main() {
  print('Начало запроса...');
  
  fetchUserName(1).then((name) {
    print('Получено имя: $name');
  }).catchError((error) {
    print('Ошибка: $error');
  });
  
  print('Запрос отправлен, ожидаем ответ...');
}
Ключевые особенности Future:

  • Выполняется один раз и возвращает одно значение (или ошибку)
  • Имеет три состояния: незавершённый (uncompleted), завершённый с результатом (completed with data), завершённый с ошибкой (completed with error)
  • Поддерживает цепочки обработки через .then() и .catchError()

Async и Await — синтаксический сахар для читаемости

Ключевые слова async и await делают асинхронный код более читаемым
и «линейным», визуально приближая его к синхронному.
// Тот же пример с использованием async/await
Future<void> main() async {
  print('Начало запроса...');
  
  try {
    final userName = await fetchUserName(1);
    print('Получено имя: $userName');
  } catch (error) {
    print('Ошибка: $error');
  }
  
  print('Запрос завершен');
}
Преимущества async/await:

  • Код выглядит как синхронный, его проще читать и сопровождать
  • Обработка ошибок через привычные блоки try/catch
  • Возможность использовать циклы и условные конструкции вместе с асинхронными вызовами

Stream — последовательность асинхронных событий

Если Future возвращает одно значение, то Stream предоставляет последовательность значений (событий), поступающих со временем.
// Пример создания простого Stream
Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // Отправляем значение в поток
  }
}
// Использование
void main() {
  final stream = countStream(5);
  
  stream.listen(
    (value) => print('Получено: $value'),
    onError: (error) => print('Ошибка: $error'),
    onDone: () => print('Поток завершен'),
  );
  
  print('Подписка оформлена, ожидаем события...');
}
Stream применим там, где данные или события приходят порционно: сообщения из WebSocket, пользовательские события, прогресс длительных операций и т.д.

Многопоточность с Isolates

В случаях, когда асинхронности на одном потоке уже недостаточно
и возникают серьёзные CPU-нагрузки, на помощь приходят изоляты
— модель многопоточности в Dart, ориентированная на безопасность
и предсказуемость.

Что такое Isolates и зачем они нужны?

Isolates (изоляты) — это механизм многопоточности в Dart, который позволяет выполнять код параллельно в разных потоках. В отличие от классических потоков, изоляты не разделяют память: у каждого изолята своё собственное пространство памяти, а взаимодействие происходит только через передачу сообщений.

Простой способ: функция compute()

Flutter предоставляет удобную функцию compute() для запуска тяжёлых вычислений в отдельном изоляте. Это самый простой способ использования изолятов.
import 'package:flutter/foundation.dart';
// Функция, которая будет выполняться в отдельном изоляте
int _heavyComputation(int number) {
  // Имитация тяжелого вычисления
  int result = 0;
  for (int i = 0; i < number * 1000000; i++) {
    result += i % 100;
  }
  return result;
}
class HeavyComputationScreen extends StatefulWidget {
  @override
  _HeavyComputationScreenState createState() => _HeavyComputationScreenState();
}
class _HeavyComputationScreenState extends State<HeavyComputationScreen> {
  int _result = 0;
  bool _isCalculating = false;
  Future<void> _startComputation() async {
    setState(() => _isCalculating = true);
    
    // Используем compute для запуска в отдельном изоляте
    final result = await compute(_heavyComputation, 1000);
    
    setState(() {
      _result = result;
      _isCalculating = false;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Тяжелые вычисления')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_isCalculating) CircularProgressIndicator(),
            SizedBox(height: 20),
            Text('Результат: $_result', style: TextStyle(fontSize: 20)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _isCalculating ? null : _startComputation,
              child: Text('Запустить вычисление'),
            ),
          ],
        ),
      ),
    );
  }
}
Ограничения compute():
  • Функция должна быть статической или глобальной
  • Аргументы и результат должны быть сериализуемыми (примитивные типы, List, Map и т.д.)
  • Подходит только для одноразовых вычислений

Полноценный Isolate для сложных задач

Для более сложных сценариев можно создать полноценный изолят, который будет работать как фоновый сервис с двусторонним обменом сообщениями.
import 'dart:isolate';
import 'dart:async';
class BackgroundService {
  late SendPort _sendPort;
  late Isolate _isolate;
  final ReceivePort _receivePort = ReceivePort();
  final StreamController<String> _messageController = StreamController.broadcast();
  Stream<String> get messages => _messageController.stream;
  Future<void> start() async {
    // Создаем изолят
    _isolate = await Isolate.spawn(
      _isolateEntry,
      _receivePort.sendPort,
    );
    // Слушаем сообщения от изолята
    _receivePort.listen((message) {
      if (message is SendPort) {
        _sendPort = message; // Получаем порт для отправки сообщений в изолят
      } else if (message is String) {
        _messageController.add(message); // Передаем сообщение в UI
      }
    });
  }
  void sendCommand(String command) {
    _sendPort.send(command);
  }
  Future<void> stop() async {
    _receivePort.close();
    _messageController.close();
    _isolate.kill(priority: Isolate.immediate);
  }
  // Эта функция выполняется в отдельном изоляте
  static void _isolateEntry(SendPort mainSendPort) {
    final ReceivePort isolateReceivePort = ReceivePort();
    mainSendPort.send(isolateReceivePort.sendPort);
    isolateReceivePort.listen((message) {
      if (message == 'START_TASK') {
        // Имитация долгой задачи
        for (int i = 0; i < 10; i++) {
          mainSendPort.send('Прогресс: ${(i + 1) * 10}%');
          // Имитация работы
          final start = DateTime.now().millisecondsSinceEpoch;
          while (DateTime.now().millisecondsSinceEpoch - start < 1000) {
            // Ждем 1 секунду
          }
        }
        mainSendPort.send('Задача завершена!');
      }
    });
  }
}
// Использование в Flutter
class IsolateExampleScreen extends StatefulWidget {
  @override
  _IsolateExampleScreenState createState() => _IsolateExampleScreenState();
}
class _IsolateExampleScreenState extends State<IsolateExampleScreen> {
  final BackgroundService _service = BackgroundService();
  List<String> _messages = [];
  bool _isRunning = false;
  @override
  void initState() {
    super.initState();
    _initService();
  }
  Future<void> _initService() async {
    await _service.start();
    _service.messages.listen((message) {
      setState(() => _messages = [..._messages, message]);
    });
  }
  @override
  void dispose() {
    _service.stop();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Фоновый сервис')),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: _isRunning 
                ? null 
                : () {
                    setState(() {
                      _isRunning = true;
                      _messages = [];
                    });
                    _service.sendCommand('START_TASK');
                  },
            child: Text('Запустить фоновую задачу'),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length,
              itemBuilder: (context, index) => ListTile(
                title: Text(_messages[index]),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Когда использовать Isolates?

Важно понимать, в каких сценариях изоляты действительно оправданы,
а где достаточно обычной асинхронности.

Используйте Isolates, когда:

  1. Обработка изображений — применение фильтров, изменение размера, кодирование/декодирование
  2. Сложные математические вычисления — машинное обучение, физические симуляции
  3. Обработка больших файлов — парсинг JSON/XML, работа с большими объёмами данных
  4. Фоновые загрузки — когда нужно продолжать работу даже при свернутом приложении
  5. Анализ данных — тяжёлые статистические расчёты, агрегация информации
Не используйте Isolates, когда:

  1. Простые асинхронные операции — достаточно Future
  2. Потоковые данные — удобнее Stream
  3. Частая коммуникация — передача сообщений между изолятами имеет накладные расходы
  4. Работа с UI — изоляты не имеют доступа к UI-потоку Flutter

Практическое применение в Flutter

В этом разделе представлены типичные сценарии использования асинхронности и многопоточности во Flutter: от сетевых запросов
до обработки изображений.

Загрузка данных из сети с отображением состояния

Один из наиболее распространённых сценариев — загрузка данных из API
с отображением различных состояний (загрузка, успех, ошибка).
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class UserListScreen extends StatefulWidget {
  @override
  _UserListScreenState createState() => _UserListScreenState();
}
class _UserListScreenState extends State<UserListScreen> {
  List<User> _users = [];
  bool _isLoading = true;
  String _errorMessage = '';
  @override
  void initState() {
    super.initState();
    _loadUsers();
  }
  Future<void> _loadUsers() async {
    try {
      setState(() {
        _isLoading = true;
        _errorMessage = '';
      });
      final response = await http.get(
        Uri.parse('https://jsonplaceholder.typicode.com/users'),
      );
      if (response.statusCode == 200) {
        final List<dynamic> data = json.decode(response.body);
        setState(() {
          _users = data.map((json) => User.fromJson(json)).toList();
          _isLoading = false;
        });
      } else {
        throw Exception('Ошибка загрузки: ${response.statusCode}');
      }
    } catch (error) {
      setState(() {
        _errorMessage = error.toString();
        _isLoading = false;
      });
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Список пользователей')),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : _errorMessage.isNotEmpty
              ? Center(child: Text('Ошибка: $_errorMessage'))
              : ListView.builder(
                  itemCount: _users.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text(_users[index].name),
                      subtitle: Text(_users[index].email),
                    );
                  },
                ),
      floatingActionButton: FloatingActionButton(
        onPressed: _loadUsers,
        child: Icon(Icons.refresh),
      ),
    );
  }
}
class User {
  final String name;
  final String email;
  User({required this.name, required this.email});
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      name: json['name'],
      email: json['email'],
    );
  }
}

Обработка изображений с помощью Isolate

import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:typed_data';
class ImageProcessingScreen extends StatefulWidget {
  @override
  _ImageProcessingScreenState createState() => _ImageProcessingScreenState();
}
class _ImageProcessingScreenState extends State<ImageProcessingScreen> {
  ui.Image? _processedImage;
  bool _isProcessing = false;
  Future<void> _processImage() async {
    setState(() => _isProcessing = true);
    
    // Загружаем тестовое изображение
    final ByteData data = await rootBundle.load('assets/sample.jpg');
    final Uint8List bytes = data.buffer.asUint8List();
    
    // Обрабатываем в отдельном изоляте
    final processedBytes = await compute(_applySepiaFilter, bytes);
    
    // Создаем изображение из обработанных байтов
    final codec = await ui.instantiateImageCodec(processedBytes);
    final frame = await codec.getNextFrame();
    
    setState(() {
      _processedImage = frame.image;
      _isProcessing = false;
    });
  }
  // Функция, которая выполняется в изоляте
  static Uint8List _applySepiaFilter(Uint8List imageBytes) {
    // Простой фильтр сепии (упрощенный пример)
    final bytes = List<int>.from(imageBytes);
    
    for (int i = 0; i < bytes.length; i += 4) {
      final r = bytes[i];
      final g = bytes[i + 1];
      final b = bytes[i + 2];
      
      // Применяем эффект сепии
      bytes[i] = ((r * 0.393) + (g * 0.769) + (b * 0.189)).clamp(0, 255).toInt();
      bytes[i + 1] = ((r * 0.349) + (g * 0.686) + (b * 0.168)).clamp(0, 255).toInt();
      bytes[i + 2] = ((r * 0.272) + (g * 0.534) + (b * 0.131)).clamp(0, 255).toInt();
    }
    
    return Uint8List.fromList(bytes);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Обработка изображений')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_processedImage != null)
              RawImage(image: _processedImage!),
            SizedBox(height: 20),
            if (_isProcessing) ...[
              CircularProgressIndicator(),
              SizedBox(height: 10),
              Text('Обработка изображения...'),
            ],
            ElevatedButton(
              onPressed: _isProcessing ? null : _processImage,
              child: Text('Обработать изображение'),
            ),
          ],
        ),
      ),
    );
  }
}

Продвинутые техники и паттерны

По мере усложнения приложения возрастает потребность грамотно комбинировать несколько асинхронных операций, управлять зависимостями и нагрузкой.

Комбинирование нескольких Future

При работе с несколькими асинхронными операциями важно правильно управлять их выполнением: что-то запускать параллельно, а что-то — строго последовательно.
Future<void> loadUserData(int userId) async {
  // Параллельное выполнение независимых запросов
  final future1 = fetchUserProfile(userId);
  final future2 = fetchUserOrders(userId);
  final future3 = fetchUserSettings(userId);
  
  // Ожидаем завершения всех Future
  final results = await Future.wait([
    future1,
    future2,
    future3,
  ], eagerError: true); // Прерываем при первой ошибке
  
  print('Профиль: ${results[0]}');
  print('Заказы: ${results[1]}');
  print('Настройки: ${results[2]}');
}
// Последовательное выполнение зависимых запросов
Future<void> processOrder(int orderId) async {
  try {
    final order = await fetchOrder(orderId);
    final user = await fetchUser(order.userId);
    final payment = await processPayment(order, user);
    
    print('Заказ обработан: $payment');
  } catch (error) {
    print('Ошибка обработки заказа: $error');
    await logError(error);
  } finally {
    await cleanupResources();
  }
}

Worker Pool с несколькими Isolates

Для обработки большого количества однотипных задач можно создать пул изолятов (worker pool) и распределять работу между ними.
import 'dart:isolate';
import 'dart:async';
class IsolatePool {
  final List<Isolate> _isolates = [];
  final List<SendPort> _ports = [];
  final int _poolSize;
  IsolatePool(this._poolSize);
  Future<void> initialize() async {
    for (int i = 0; i < _poolSize; i++) {
      final receivePort = ReceivePort();
      final isolate = await Isolate.spawn(
        _workerFunction,
        receivePort.sendPort,
      );
      
      final sendPort = await receivePort.first;
      
      _isolates.add(isolate);
      _ports.add(sendPort as SendPort);
    }
  }
  Future<T> execute<T>(Function task, dynamic argument) async {
    final completer = Completer<T>();
    final responsePort = ReceivePort();
    
    // Выбираем изолят по кругу (простой load balancing)
    final sendPort = _ports.first;
    sendPort.send({
      'task': task,
      'argument': argument,
      'responsePort': responsePort.sendPort,
    });
    
    responsePort.listen((message) {
      if (message is T) {
        completer.complete(message);
      } else if (message is Exception) {
        completer.completeError(message);
      }
      responsePort.close();
    });
    
    return completer.future;
  }
  static void _workerFunction(SendPort mainSendPort) {
    final receivePort = ReceivePort();
    mainSendPort.send(receivePort.sendPort);
    
    receivePort.listen((message) {
      final task = message['task'] as Function;
      final argument = message['argument'];
      final responsePort = message['responsePort'] as SendPort;
      
      try {
        final result = task(argument);
        responsePort.send(result);
      } catch (e) {
        responsePort.send(Exception(e.toString()));
      }
    });
  }
  void dispose() {
    for (final isolate in _isolates) {
      isolate.kill();
    }
  }
}

Когда что использовать

Выбор правильного инструмента под конкретную задачу помогает упростить архитектуру и избежать преждевременного усложнения кода.

  • Для операций, которые выполняются один раз и возвращают результат

Используйте Future, когда необходимо выполнить операцию и получить единичный результат. Примеры: загрузка данных из сети, чтение файла, выполнение запроса к базе данных. Future идеально подходит для сценариев «запрос–ответ».

  • Для последовательностей событий, поступающих со временем

Используйте Stream, когда ожидается несколько значений, поступающих асинхронно. Примеры: отслеживание нажатий, получение обновлений местоположения, сообщения из WebSocket, отслеживание прогресса загрузки файла.
  • Для тяжёлых вычислений и CPU-интенсивных задач

Используйте Isolate, когда операция может заблокировать основной поток: обработка изображений, сложные математические расчёты, парсинг больших файлов, задачи машинного обучения.

  • Для комбинации подходов

В реальных приложениях часто приходится комбинировать разные механизмы:

  • Использовать Future для загрузки данных
  • Обрабатывать данные в Isolate, если они требуют тяжёлых вычислений
  • Отправлять результаты через Stream для реактивного обновления UI

Обработка ошибок в асинхронном коде

Асинхронный код особенно чувствителен к ошибкам: неперехваченные исключения могут проявляться в неожиданных местах. Важно планировать стратегию обработки ошибок заранее.

Стратегии обработки ошибок

Ниже приведены типовые подходы к обработке ошибок в асинхронном коде с использованием async/await, Future-цепочек и изолятов.
// 1. Обработка в async/await
Future<void> loadData() async {
  try {
    final data = await fetchData();
    await processData(data);
  } on SocketException catch (e) {
    print('Ошибка сети: $e');
    showNetworkError();
  } on FormatException catch (e) {
    print('Ошибка формата данных: $e');
    showDataError();
  } catch (e) {
    print('Неизвестная ошибка: $e');
    showGenericError();
  }
}
// 2. Обработка в Future
Future<void> loadUserProfile() {
  return fetchProfile()
    .then((profile) => validateProfile(profile))
    .then((validated) => saveProfile(validated))
    .catchError((error) {
      print('Ошибка загрузки профиля: $error');
      return Profile.defaultProfile();
    }, test: (error) => error is! CriticalError)
    .whenComplete(() => log('Загрузка профиля завершена'));
}
// 3. Обработка в Isolate
Future<T> runInIsolateWithErrorHandling<T>(
  Function task, 
  dynamic argument,
) async {
  try {
    return await compute(task, argument);
  } on IsolateSpawnException catch (e) {
    print('Не удалось запустить изолят: $e');
    rethrow;
  } catch (e) {
    print('Ошибка в изоляте: $e');
    rethrow;
  }
}

Лучшие практики и рекомендации

Соблюдение набора простых правил помогает поддерживать асинхронный код чистым, предсказуемым и удобным для сопровождения.

  1. Используйте async/await для улучшения читаемости — особенно для сложных цепочек асинхронных операций.
  2. Освобождайте ресурсы — закрывайте StreamController и отменяйте подписки в dispose(), корректно останавливайте изоляты.
  3. Избегайте блокировки основного потока — выносите тяжёлые вычисления в Isolate.
  4. Используйте debounce и throttle для обработки частого пользовательского ввода (поиск, автодополнение и т.п.).
  5. Тестируйте асинхронный код — Dart предоставляет удобные средства для тестирования Future, Stream и Isolate.

Заключение

В итоге асинхронность и многопоточность нужны ради того, чтобы приложение оставалось плавным и предсказуемым для пользователя.
Важно выстраивать архитектуру постепенно: сначала опираться на Future
и async/await, а уже потом, по мере роста требований и нагрузки, подключать Stream, Isolate и более сложные подходы там, где они действительно дают ощутимую пользу.
Оценить материал
Остальные статьи по flutter