Как работает Future.wait в Dart и для каких задач он полезен во 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.

Ответ 18+ 🔞

А, вот эта штука, Future.wait()! Ну, смотри, представь себе такую картину. Ты стоишь на кухне, голодный как собака, и тебе надо и макароны сварить, и котлеты пожарить, и чайник вскипятить. Можно делать всё по очереди — это как await для каждой операции. Но это же, ёпта, овердохуища времени займёт! Ты будешь ждать, пока вода закипит, потом ставить макароны, потом сковородку гре... Короче, терпения ноль ебать.

А можно по-взрослому. Включил все конфорки сразу, на одну — чайник, на другую — кастрюлю с водой для макарон, на третью — сковородку. И всё это греется параллельно. Вот Future.wait() — это ты как раз шеф-повар на такой кухне. Ты говоришь: "Так, все мои асинхронные операции (Future), начинайте работать одновременно, а я подожду, пока все доедят".

Простой пример, чтобы въехать:

Future<String> скачатьАватарку() async {
  await Future.delayed(Duration(seconds: 1)); // Представь, что это сетевой запрос
  return 'база данных вернула ссылку на фото';
}

Future<int> получитьБаллы() async {
  await Future.delayed(Duration(seconds: 2));
  return 100500;
}

Future<void> main() async {
  // Запускаем два запроса ПАРАЛЛЕЛЬНО. Вместо 3 секунд (1+2) ждём всего ~2.
  final List<dynamic> результаты = await Future.wait([
    скачатьАватарку(),
    получитьБаллы(),
  ]);

  print('Аватар: ${результаты[0]}, Баллы: ${результаты[1]}');
  // Вывод через ~2 секунды: Аватар: база данных..., Баллы: 100500
}

Важные моменты, где можно обжечься:

  1. Параллельность: Да, всё стартует сразу. Это не цепочка await.
  2. Ожидание: Ты получишь результат, только когда последний из этих Future отчитается. Даже если первый управился за милисекунду.
  3. Главная засада — ошибки. Представь, ты послал трёх курьеров: одного за пиццей, второго за колой, третьего за салфетками. Используешь Future.wait() за всеми. И тут второго курьера с колой сбивает маршрутка. Что делает Future.wait? Он такой: "Всё, доверия ебать ноль!" — и тебе прилетает одна большая ошибка "курьер №2 мертв". И ты даже не узнаешь, что пицца-то уже приехала и салфетки тоже! Результаты успешных Future просто накрываются медным тазом.
// ❌ Проблема: если скачатьАватарку() упадёт, баллы мы тоже не увидим.
try {
  final results = await Future.wait([скачатьАватарку(), получитьБаллы()]);
} catch (e) {
  print('Один из запросов сдох: $e'); // И всё, хуй что поймёшь, какой именно.
}

Где это реально полезно во Flutter:

  1. Загрузка всей хуйни для экрана сразу:

    Future<void> загрузитьЭкран() async {
      // Параллельно дербаним сервер на три части: юзер, посты, настройки.
      final results = await Future.wait([
        _api.получитьЮзера(),
        _api.получитьЛенту(),
        _localStorage.загрузитьНастройки(),
      ]);
    
      setState(() {
        _юзер = results[0] as User;
        _посты = results[1] as List<Post>;
        _настройки = results[2] as Settings;
      });
      // Экран построился из всего сразу, а не кусками.
    }
  2. Хитрая жопа: обработка ошибок для каждого отдельно. Чтобы не терять результаты из-за одного говнаря.

    Future<void> загрузитьБезопасно() async {
      // Каждый Future оборачиваем так, чтобы он не падал, а возвращал "результат или ошибку"
      final futures = [
        _запрос1().then((v) => Успех(v)).catchError((e) => Провал(e)),
        _запрос2().then((v) => Успех(v)).catchError((e) => Провал(e)),
      ];
    
      final List<dynamic> результаты = await Future.wait(futures); // Тут уже ничего не упадёт
    
      for (final res in результаты) {
        if (res is Успех) {
          print('Опа, данные: ${res.значение}');
        } else if (res is Провал) {
          print('Ну вот, опять: ${res.ошибка}');
        }
      }
    }
  3. Отправка аналитики, когда похуй на результат. Кинул кучу событий в параллель и забыл.

    Future<void> отправитьСобытия() async {
      // Нам не нужно ждать ответа от сервера аналитики. unawaited чтобы не ругался линтер.
      unawaited(Future.wait([
        _analytics.событие('экран_открыт'),
        _analytics.событие('кнопка_тыкнута', параметры: {'id': 123}),
      ]));
      // Отправили и пошли дальше, не ждём.
    }

Если тебе прям вот надо, чтобы каждый Future жил своей жизнью и ошибки одного не убивали других — тогда Future.wait в лоб не катит. Смотри в сторону Future.forEach с индивидуальными try-catch внутри или ищи пакеты вроде async, там есть FutureGroup. Но для большинства случаев, когда тебе нужны все результаты разом, Future.wait() — это, бля, пизда рулю. Главное — помни про ошибки, а то сам от себя охуеешь, когда всё падает из-за одной ерунды.