
При разработке 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, который позволяет выполнять код параллельно в разных потоках. В отличие от классических потоков, изоляты не разделяют память: у каждого изолята своё собственное пространство памяти, а взаимодействие происходит только через передачу сообщений.
Зачем использовать Isolates:
Для выполнения тяжёлых вычислений (обработка изображений, сложные алгоритмы)
Для обработки больших объёмов данных без блокировки UI
Для фоновых задач, которые должны работать независимо от основного потока
Когда Future недостаточно, потому что операция реально блокирующая и долгая
Простой способ: функция 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, когда:
Обработка изображений — применение фильтров, изменение размера, кодирование/декодирование
Сложные математические вычисления — машинное обучение, физические симуляции
Обработка больших файлов — парсинг JSON/XML, работа с большими объёмами данных
Фоновые загрузки — когда нужно продолжать работу даже при свернутом приложении
Анализ данных — тяжёлые статистические расчёты, агрегация информации
Не используйте Isolates, когда:
Простые асинхронные операции — достаточно Future
Потоковые данные — удобнее Stream
Частая коммуникация — передача сообщений между изолятами имеет накладные расходы
Работа с 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
Обработка изображений часто является CPU-интенсивной задачей. Вынос её в изолят позволяет не блокировать интерфейс.
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
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;
}
}
Лучшие практики и рекомендации
Соблюдение набора простых правил помогает поддерживать асинхронный код чистым, предсказуемым и удобным для сопровождения.
Всегда обрабатывайте ошибки — не оставляйте неперехваченные исключения в асинхронном коде.
Используйте async/await для улучшения читаемости — особенно для сложных цепочек асинхронных операций.
Освобождайте ресурсы — закрывайте StreamController и отменяйте подписки в dispose(), корректно останавливайте изоляты.
Избегайте блокировки основного потока — выносите тяжёлые вычисления в Isolate.
Используйте debounce и throttle для обработки частого пользовательского ввода (поиск, автодополнение и т.п.).
Выбирайте правильный инструмент для задачи — Future для единичных операций, Stream для потоков данных, Isolate для тяжёлых вычислений.
Тестируйте асинхронный код — Dart предоставляет удобные средства для тестирования Future, Stream и Isolate.
Заключение
В итоге асинхронность и многопоточность нужны ради того, чтобы приложение оставалось плавным и предсказуемым для пользователя. Важно выстраивать архитектуру постепенно: сначала опираться на Future и async/await, а уже потом, по мере роста требований и нагрузки, подключать Stream, Isolate и более сложные подходы там, где они действительно дают ощутимую пользу.