В чем разница между Stream и Iterable в Dart?

Ответ

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

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

// Пример из реального проекта: обработка списка пользователей
List<User> users = [/* ... */];

// Цепочка преобразований с Iterable
final activeAdults = users
    .where((user) => user.isActive)
    .where((user) => user.age >= 18)
    .map((user) => user.toJson())
    .toList();

// Ленивое вычисление - данные обрабатываются только при итерации
final expensiveCalculation = users.map((user) {
  print('Вычисление для ${user.name}');
  return calculateSomethingExpensive(user);
});
// Ничего не выведется пока не начнем итерацию

Stream представляет асинхронную последовательность событий. В Flutter-приложениях я использую Stream для работы с событиями UI и сетевыми запросами:

// Пример: обработка потока событий от Firebase Firestore
Stream<List<Message>> getMessages(String chatId) {
  return FirebaseFirestore.instance
      .collection('chats')
      .doc(chatId)
      .collection('messages')
      .orderBy('timestamp', descending: true)
      .limit(50)
      .snapshots()
      .map((snapshot) => snapshot.docs
          .map((doc) => Message.fromFirestore(doc))
          .toList());
}

// Использование в виджете
StreamBuilder<List<Message>>(
  stream: getMessages(chatId),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return ListView.builder(
        itemCount: snapshot.data!.length,
        itemBuilder: (context, index) {
          return MessageWidget(snapshot.data![index]);
        },
      );
    }
    return CircularProgressIndicator();
  },
)

Основные различия в моей практике:

  1. Синхронность vs Асинхронность:

    • Iterable: данные доступны сразу, блокирует выполнение
    • Stream: данные поступают со временем, не блокирует UI
  2. Многократность использования:

    • Iterable можно итерировать многократно
    • Single-subscription Stream можно слушать только один раз
  3. Использование в Flutter:

    • Iterable: для статических данных, фильтрации, преобразований
    • Stream: для real-time обновлений, событий пользователя, WebSockets
  4. Ошибки и завершение:

    • Iterable либо возвращает все значения, либо бросает исключение
    • Stream может эмитировать данные, ошибки и сигнал о завершении

В сложных приложениях я часто комбинирую оба подхода, например, преобразуя Stream данных в Iterable для batch-обработки.

Ответ 18+ 🔞

Блин, смотри, в Dart и Flutter я с этими коллекциями работаю постоянно, но они, блядь, как день и ночь — для абсолютно разных задач.

Iterable — это такая синхронная штука, которая лениво вычисляется. Я её юзаю, когда данные уже в памяти и их надо просто обработать. Ну, типа, отфильтровать список пользователей:

// Пример из реального проекта: обработка списка пользователей
List<User> users = [/* ... */];

// Цепочка преобразований с Iterable
final activeAdults = users
    .where((user) => user.isActive)
    .where((user) => user.age >= 18)
    .map((user) => user.toJson())
    .toList();

// Ленивое вычисление - данные обрабатываются только при итерации
final expensiveCalculation = users.map((user) {
  print('Вычисление для ${user.name}');
  return calculateSomethingExpensive(user);
});
// Ничего не выведется пока не начнем итерацию

Вот эта лень — это охуенно, потому что пока ты не начал по нему итерироваться, никаких тяжёлых вычислений не происходит. Удобно, ёпта.

А вот Stream — это уже совсем другая история, асинхронная. Представь себе пожарный гидрант, из которого данные льются не когда ты захочешь, а когда им вздумается. В Flutter без них нихуя — все эти real-time обновления, события от пользователя, данные из сети. Вот смотри, как я с Firestore работаю:

// Пример: обработка потока событий от Firebase Firestore
Stream<List<Message>> getMessages(String chatId) {
  return FirebaseFirestore.instance
      .collection('chats')
      .doc(chatId)
      .collection('messages')
      .orderBy('timestamp', descending: true)
      .limit(50)
      .snapshots()
      .map((snapshot) => snapshot.docs
          .map((doc) => Message.fromFirestore(doc))
          .toList());
}

// Использование в виджете
StreamBuilder<List<Message>>(
  stream: getMessages(chatId),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return ListView.builder(
        itemCount: snapshot.data!.length,
        itemBuilder: (context, index) {
          return MessageWidget(snapshot.data![index]);
        },
      );
    }
    return CircularProgressIndicator();
  },
)

А теперь, блядь, основные отличия, чтобы голова не ебола:

  1. Синхронный vs Асинхронный: Iterable — данные тут, бери и работай, но поток выполнения ждёт. Stream — данные приплывут когда-нибудь, а UI тем временем не тормозит. Это, блин, принципиально.
  2. Можно тыкать много раз или один: По Iterable можно пройтись сто раз — он не обидится. А single-subscription Stream — это как одноразовая зажигалка, послушал один раз и всё, накрылся медным тазом, нужно новый создавать.
  3. Где юзать: Iterable — для всяких статических данных, фильтраций, map/reduce. Stream — это когда тебе из бэкенда каждую секунду что-то прилетает, или кнопку нажали, или таймер сработал. Без Stream в современном приложении — просто пидарас шерстяной.
  4. Как заканчивается: Iterable либо отдал все данные, либо выбросил исключение — и всё, приехали. Stream же может спокойно сказать: «Всё, ребята, я закончил» (onDone), или «Ой, бля, ошибка!» (onError) — и это нормальный рабочий процесс.

В общем, в сложных проектах я их часто миксую. Например, из Stream налетает овердохуища данных, а я их пачками выгребаю и через Iterable методы обрабатываю. Главное — понимать, где какой инструмент вставить, чтобы не получилась пиздопроебибна архитектура.