> Есть ли в Dart специальный тип Stream (Flutter)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Верме

Стек: Flutter

> Пример ответа

Короткий ответ

Да, в Dart есть специальный тип Stream<T> - это асинхронная последовательность событий. Он является ключевой частью асинхронной модели языка наряду с Future<T>. Stream позволяет передавать данные с течением времени: пользовательские вводы, ответы от API, события таймера, изменения состояния. В Flutter Stream используется повсеместно: от StreamBuilder до Bloc и Riverpod. Основные способы создания - Stream.fromIterable, Stream.periodic, StreamController, а также async-генераторы через async*.

Подробное объяснение

Stream<T> - это абстрактный класс в dart:async, представляющий асинхронный поток данных. В отличие от Future, который завершается одним значением или ошибкой, Stream может испускать множество событий (data, error, done) в течение времени.

Ключевые характеристики:

  • Single-subscription vs broadcast: обычный Stream можно слушать только один раз (single-subscription). Для множества подписчиков нужен broadcast stream, создаваемый через StreamController.broadcast() или asBroadcastStream().
  • Backpressure: Dart не имеет встроенного механизма backpressure, как в RxJava. Если consumer медленнее producer, события буферизируются. Это важно учитывать при работе с большими потоками данных.
  • Асинхронность: события доставляются асинхронно, но порядок сохраняется. Подписка на stream не блокирует выполнение.
  • Операторы: в стандартной библиотеке нет богатого набора операторов, как в RxDart. Но есть базовые: map, where, listen, fold, forEach, take, skip, expand, timeout, transform (для StreamTransformer).

Создание stream:

// Из коллекции
Stream<int> fromList = Stream.fromIterable([1, 2, 3]);

// Периодический
Stream<int> timer = Stream.periodic(Duration(seconds: 1), (i) => i);

// Через StreamController
final controller = StreamController<int>();
controller.add(1);
controller.close();

// Async-генератор
Stream<int> countDown(int n) async* {
  for (int i = n; i > 0; i--) {
    yield i;
    await Future.delayed(Duration(seconds: 1));
  }
}

Важные нюансы для senior-уровня:

  • StreamController требует явного закрытия, иначе возможна утечка памяти.
  • При использовании listen нужно управлять подпиской (StreamSubscription) и отменять её в dispose.
  • Stream - это pull-based модель в терминах Rx: consumer сам решает, когда слушать. Но внутри используется push-механизм.
  • StreamTransformer позволяет создавать переиспользуемые трансформации, например, для дебаунса или буферизации.

На практике

В Flutter Stream используется для:

  • Реактивного состояния: StreamBuilder слушает stream и перестраивает UI при каждом событии.
  • Bloc pattern: Bloc принимает события через Stream и отдаёт состояния через Stream.
  • Работа с Firebase: FirebaseFirestore.instance.collection(...).snapshots() возвращает Stream<QuerySnapshot>.
  • Обработка пользовательского ввода: дебаунс через StreamTransformer для поиска.
  • Инжекция зависимостей: StreamProvider в Riverpod.

На практике важно помнить:

  • Для UI-слоя используй StreamBuilder или StreamProvider, а не ручной listen - иначе легко забыть отменить подписку.
  • Для сложных операций (combineLatest, debounce, throttle) используй RxDart - это стандарт де-факто для реактивного программирования в Dart.
  • При работе с StreamController всегда закрывай его в dispose, иначе stream будет жить вечно.
  • Если stream испускает ошибки, обязательно обрабатывай их через onError, иначе они будут проброшены как unhandled exception.

Пример кода

// Пример с StreamBuilder в Flutter
class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  final _controller = StreamController<int>();
  int _count = 0;

  void _increment() {
    _count++;
    _controller.add(_count);
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        StreamBuilder<int>(
          stream: _controller.stream,
          initialData: 0,
          builder: (context, snapshot) {
            return Text('Count: ${snapshot.data}');
          },
        ),
        ElevatedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

// Пример с async* генератором и трансформацией
Stream<int> _numbers() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(milliseconds: 500));
    yield i;
  }
}

Stream<int> _evenNumbers() {
  return _numbers().where((n) => n.isEven);
}

// Использование с debounce через StreamTransformer
Stream<String> _debounce(Stream<String> source, Duration duration) {
  return source.transform(
    StreamTransformer.fromHandlers(
      handleData: (value, sink) {
        // реализация debounce опущена для краткости
      },
    ),
  );
}

Как отвечать на собеседовании

Начни с прямого ответа: "Да, в Dart есть тип Stream<T>". Затем кратко объясни разницу между Future и Stream. Покажи, что понимаешь асинхронную модель Dart. Упомяни StreamController, async* генераторы, StreamBuilder. Для senior-уровня добавь:

  • различие между single-subscription и broadcast;
  • проблему backpressure и как её решать (например, через StreamQueue или RxDart);
  • управление жизненным циклом подписок;
  • когда использовать Stream вместо Future или ChangeNotifier.

Если спросят про альтернативы - упомяни Stream в контексте Isolate (для параллельной обработки данных) и StreamChannel для двусторонней связи.

Что проверяет интервьюер

Интервьюер проверяет:

  • базовое понимание асинхронности в Dart;
  • умение работать с StreamController и подписками;
  • знание практических паттернов в Flutter (StreamBuilder, Bloc, Riverpod);
  • понимание проблем (утечки памяти, backpressure, обработка ошибок);
  • способность объяснить разницу между Stream и Future, Stream и ChangeNotifier.

Для senior-уровня важно показать, что ты не просто знаешь API, а понимаешь внутреннюю механику: как работает event loop, как stream взаимодействует с микротасками, когда события доставляются синхронно, а когда асинхронно.

Типичные ошибки

  • Не закрывать StreamController - приводит к утечке памяти.
  • Не отменять подписку - при использовании listen в State без сохранения StreamSubscription.
  • Путать Stream и Future - пытаться использовать await на stream (это работает, но только для первого события).
  • Игнорировать ошибки - не передавать onError в listen, что приводит к crash.
  • Использовать single-subscription stream для нескольких слушателей - получать ошибку "Stream has already been listened to".
  • Не учитывать backpressure - при работе с большими данными (например, чтение файла) без StreamTransformer или буферизации.
  • Создавать stream на каждый билд - в build методе, что приводит к пересозданию подписок.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью