Kodokon kodokon.com

Асинхронность в Dart: Future, async/await, FutureBuilder

Освой Future, async/await и FutureBuilder, не блокируя интерфейс и не отправляя свои запросы заново по случайности.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

Dart выполняет твоё приложение в одном потоке, которым управляет цикл событий. Future - это не поток: это обещание значения, которое придёт позже. await никогда не блокирует этот поток - он приостанавливает текущую функцию, возвращает управление циклу (интерфейс продолжает работать), а затем возобновляет её, когда приходит результат. Следствие: async/await ничего не распараллеливает для чистых вычислений; для тяжёлой работы с процессором нужен изолят.

DART
Future<double> fetchRate() async {
  await Future<void>.delayed(
    const Duration(milliseconds: 300),
  );
  return 1.08;
}

Future<void> main() async {
  try {
    final rate = await fetchRate();
    print('EUR/USD rate: $rate');
  } on Exception catch (error) {
    print('Rate loading failed: $error');
  }
}
Асинхронную функцию потребляют через await и try/catch.

Два идущих подряд await выполняются последовательно: второй стартует только после первого. Когда операции независимы, запускай их вместе через Future.wait. Компромисс, который надо знать: Future.wait работает по принципу fail-fast - первая же ошибка отклоняет всё целиком, и результаты остальных future теряются, даже если они успешны. Если тебе нужны частичные результаты, обрабатывай ошибку внутри каждого future.

DART
Future<String> fetchProfile() async => 'profile';

Future<String> fetchOrders() async => 'orders';

Future<void> main() async {
  final [profile, orders] = await Future.wait(
    [fetchProfile(), fetchOrders()],
  );
  print('$profile / $orders');
}
Распараллеливание через Future.wait и деструктуризацию Dart 3.

Со стороны интерфейса FutureBuilder связывает Future с build через snapshot. Ловушка номер один на практике: создать future прямо в build. Тогда каждая перерисовка - хватит простого setState у родителя - будет заново отправлять запрос. Сохраняй future в initState и всегда проверяй ошибку раньше данных.

DART
import 'package:flutter/material.dart';

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});

  @override
  State<ProfileScreen> createState() =>
      _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  late final Future<String> _nameFuture;

  @override
  void initState() {
    super.initState();
    _nameFuture = _loadName();
  }

  Future<String> _loadName() async {
    await Future<void>.delayed(
      const Duration(seconds: 1),
    );
    return 'Ada Lovelace';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _nameFuture,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        if (!snapshot.hasData) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
        return Text(snapshot.data!);
      },
    );
  }
}
Future создаётся один-единственный раз, в initState.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что на самом деле делает await в главном потоке?
    • Блокирует поток, пока Future не разрешится.
    • Автоматически переносит вычисление во вторичный поток.
    • Приостанавливает текущую функцию и возвращает управление циклу событий: интерфейс продолжает работать.
  2. Что произойдёт с Future.wait([a(), b()]), если a() завершится ошибкой?
    • Future.wait отклоняется с ошибкой из a(); результат b() теряется.
    • Future.wait дожидается конца b(), а затем возвращает только его результат.
    • Ошибка игнорируется, пока b() успешен.
  3. Почему не стоит писать future: fetchUser() прямо в build вместе с FutureBuilder?
    • build не может содержать асинхронный вызов, код не компилируется.
    • Каждая перерисовка создавала бы новый Future и, значит, заново отправляла бы сетевой запрос.
    • snapshot навсегда застрял бы в состоянии ожидания.