Что такое Stream в Dart и как он используется во Flutter?

Ответ

Stream в Dart — это последовательность асинхронных событий. Это фундаментальная абстракция для реактивного программирования во Flutter, используемая для обработки данных, поступающих со временем: пользовательские события, ответы от сервера, показания датчиков, обновления состояния.

Базовый пример создания и подписки:

// Создание простого Stream с использованием async*
Stream<int> countDown(int from) async* {
  for (int i = from; i >= 0; i--) {
    await Future.delayed(const Duration(seconds: 1));
    yield i; // Каждую секунду выдаём новое значение
  }
}

void main() {
  final stream = countDown(5);
  // Подписка на поток
  final subscription = stream.listen((value) {
    print('Осталось: $value сек');
  }, onDone: () {
    print('Отсчёт завершён!');
  });
  // subscription.cancel(); // Для отмены подписки
}

Ключевые операции с Stream (трансформации):

stream
  .where((value) => value.isEven) // Фильтрация
  .map((value) => value * 2)      // Преобразование
  .take(3)                        // Ограничение количества событий
  .listen(print);

Использование во Flutter с StreamBuilder: StreamBuilder — это виджет, который автоматически перестраивает UI в ответ на новые данные из Stream.

class DataDisplayWidget extends StatelessWidget {
  final Stream<String> dataStream;

  const DataDisplayWidget({super.key, required this.dataStream});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<String>(
      stream: dataStream,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Ошибка: ${snapshot.error}');
        }
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            return const CircularProgressIndicator();
          default:
            return Text('Полученные данные: ${snapshot.data}');
        }
      },
    );
  }
}

Важные концепции:

  • Холодный (Cold) Stream: Начинает генерировать события при появлении первого слушателя (как в примере с async*).
  • Горячий (Hot) Stream: Генерирует события независимо от наличия слушателей (например, StreamController.broadcast()).
  • Управление подпиской: Крайне важно отменять подписки (subscription.cancel()) в методе dispose() StatefulWidget или использовать пакеты вроде rxdart, которые предоставляют автоматическое управление жизненным циклом.

Streams лежат в основе многих популярных менеджеров состояний Flutter, таких как BLoC (где StreamController используется для приёма событий и эмиссии состояний).

Ответ 18+ 🔞

Давай разберемся с этими вашими стримами в Dart, а то народ путается, как будто впервые про асинхронность слышит. Представь себе конвейерную ленту, ёпта. Не простую, а такую, по которой данные плывут не сразу все, а по кусочку, с паузами. Вот это и есть Stream. Штука архиполезная, когда у тебя что-то приходит со временем: клики юзера, ответы от сервера, который думает, как ленивая жопа, или, например, показания с гироскопа телефона.

Смотри, как эту ленту самому запустить и слушать:

// Делаем свою "ленту", которая отсчитывает секунды
Stream<int> countDown(int from) async* {
  for (int i = from; i >= 0; i--) {
    // Ждём секунду, типа задержка для драмы
    await Future.delayed(const Duration(seconds: 1));
    yield i; // А вот и наш "кусочек" данных выезжает на ленту!
  }
}

void main() {
  final stream = countDown(5);
  // Теперь подписываемся на ленту и смотрим, что ползёт
  final subscription = stream.listen((value) {
    print('Осталось: $value сек');
  }, onDone: () {
    print('Отсчёт завершён! Ядрёна вошь, наконец-то!');
  });
  // subscription.cancel(); // А этой строчкой мы ленту просто рубим, если надоело
}

Ну а дальше начинается магия, блядь. С этой лентой можно делать что угодно, прежде чем данные дойдут до слушателя. Фильтровать, преобразовывать — в общем, полный картель.

Основные приёмы уличной магии (трансформации):

stream
  .where((value) => value.isEven) // Отфильтруем, оставим только чётные. Нахуй нечётные!
  .map((value) => value * 2)      // Каждое значение умножаем на два. Просто потому что можем.
  .take(3)                        // Берём только первые три штуки, а остальное — да похуй.
  .listen(print);                 // И смотрим, что в итоге вышло.

А теперь самое охуенное — как это влепить в интерфейс Flutter! Для этого есть виджет StreamBuilder. Он, сука, умный: сам подписывается на твою ленту, слушает её и перерисовывает часть экрана, как только приезжает новый кусок данных. Вообще красота.

class DataDisplayWidget extends StatelessWidget {
  final Stream<String> dataStream; // Наша волшебная лента с данными

  const DataDisplayWidget({super.key, required this.dataStream});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<String>(
      stream: dataStream, // Сюда её подаём
      builder: (context, snapshot) {
        // snapshot — это как снимок текущего состояния ленты
        if (snapshot.hasError) {
          // Если всё накрылось медным тазом и приехала ошибка
          return Text('Ошибка: ${snapshot.error}');
        }
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            // Лента есть, но данных ещё нихуя не приехало. Показываем крутилку.
            return const CircularProgressIndicator();
          default:
            // Всё, данные приехали! Показываем их.
            return Text('Полученные данные: ${snapshot.data}');
        }
      },
    );
  }
}

И пара важных концепций, чтобы не облажаться:

  • Холодный (Cold) Stream: Это как кассета. Пока не нажмёшь play (не добавишь listen), музыка не играет. Данные начинают генерироваться только для первого слушателя. Как в нашем примере с async*.
  • Горячий (Hot) Stream: Это как радиоэфир. Данные льются постоянно, независимо от того, слушает тебя кто-то или нет. Например, StreamController.broadcast() — это оно.
  • Управление подпиской — это пиздец важно: Если ты в виджете подписался на ленту, ты ОБЯЗАН отписаться, когда виджет помирает. Иначе будет утечка памяти, а твое приложение начнёт жрать оперативку, как не в себя. Делается это через subscription.cancel() в методе dispose(). А ещё есть умные пакеты вроде rxdart, которые могут делать это за тебя, что очень удобно, чувак.

Кстати, на этих самых стримах, блядь, построены целые менеджеры состояний. Тот же BLoC — это по сути один большой, хитрожопый StreamController, который принимает события, перемалывает их и выплёвывает новые состояния в виде стрима. Так что разобраться тут — это прям основа основ.