Как работает Future.wait в Dart и для каких задач он полезен во Flutter?

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

Ответ

Future.wait() — это статический метод класса Future, который позволяет параллельно запустить несколько асинхронных операций и дождаться результатов выполнения всех из них. Он принимает Iterable<Future> и возвращает Future<List<T>>, где T — это тип результата каждого из переданных Future.

Базовый пример:

Future<String> fetchUserProfile() async {
  await Future.delayed(Duration(seconds: 1)); // Имитация сетевого запроса
  return 'John Doe';
}

Future<int> fetchUserScore() async {
  await Future.delayed(Duration(seconds: 2));
  return 95;
}

Future<void> main() async {
  // Запускаем оба Future параллельно. Общее время ~2 сек, а не 3.
  final List<dynamic> results = await Future.wait([
    fetchUserProfile(),
    fetchUserScore(),
  ]);

  print('Profile: ${results[0]}, Score: ${results[1]}');
  // Вывод через ~2 секунды: Profile: John Doe, Score: 95
}

Ключевое поведение и обработка ошибок:

  1. Параллельное выполнение: Все Future в списке начинают выполняться немедленно, а не последовательно.
  2. Завершение: Возвращаемый Future завершится только когда завершатся все переданные Future.
  3. Ошибки: Если любой из Future завершится с ошибкой, то весь Future.wait немедленно завершится с этой ошибкой, и результаты остальных Future будут потеряны.
    // ❌ Проблема: если fetchUserProfile упадёт, мы не получим даже успешный результат fetchUserScore.
    try {
      final results = await Future.wait([fetchUserProfile(), fetchUserScore()]);
    } catch (e) {
      print('Один из запросов упал: $e');
    }

Практические сценарии использования во Flutter:

  1. Параллельная загрузка данных для экрана:

    Future<void> loadInitialData() async {
      // Параллельно загружаем пользователя, настройки и список друзей
      final results = await Future.wait([
        _userRepository.getCurrentUser(),
        _settingsRepository.fetchSettings(),
        _friendsRepository.getFriendList(),
      ]);
    
      setState(() {
        _currentUser = results[0] as User;
        _settings = results[1] as AppSettings;
        _friends = results[2] as List<Friend>;
      });
    }
  2. Надёжная обработка ошибок с try-catch для каждого Future:

    Future<void> loadDataSafely() async {
      // Создаём Future, которые никогда не падают, а возвращают Result объект
      final futures = [
        _fetchData1().then((value) => Result.success(value)).catchError((e) => Result.failure(e)),
        _fetchData2().then((value) => Result.success(value)).catchError((e) => Result.failure(e)),
      ];
    
      final List<Result> results = await Future.wait(futures);
    
      // Теперь можем обработать успешные и неуспешные результаты независимо
      for (final result in results) {
        if (result.isSuccess) {
          print('Данные: ${result.value}');
        } else {
          print('Ошибка загрузки: ${result.error}');
        }
      }
    }
  3. Отправка нескольких независимых аналитических событий:

    Future<void> logAnalyticsEvents() async {
      // Нам не важен результат, и мы не хотим ждать. Используем wait в isolate.
      unawaited(Future.wait([
        _analytics.logEvent('screen_opened'),
        _analytics.logEvent('user_action', params: {'action': 'click'}),
      ]));
    }

Альтернатива для независимой обработки ошибок: Если нужно, чтобы каждый Future завершился независимо, и вы могли обработать все результаты и ошибки, рассмотрите использование Future.forEach с индивидуальным try-catch внутри цикла или пакет async с функцией FutureGroup.