StreamBuilder — виджет Flutter, который автоматически перестраивает интерфейс при получении новых данных из асинхронного потока. В отличие от FutureBuilder, работающего с однократным результатом, StreamBuilder поддерживает непрерывное обновление UI на протяжении всего жизненного цикла Stream. По данным официальной документации Flutter (2026), StreamBuilder используется в приложениях реального времени: чаты, ленты новостей, мониторинг датчиков и финансовые тикеры. Это ключевой инструмент реактивного программирования, где UI отражает состояние данных без ручных вызовов setState.
Главное
StreamBuilder — это виджет из пакета Flutter SDK, который подписывается на Stream и перестраивает свой дочерний элемент при каждом новом событии потока. StreamBuilder принимает объект Stream и возвращает widget на основе последнего snapshot, полученного из потока.
В архитектуре Flutter StreamBuilder относится к группе Builder-виджетов, которые отделяют построение UI от состояния данных. В отличие от StatefulWidget, где изменение состояния требует явного вызова setState, StreamBuilder реагирует на асинхронные события автоматически, что упрощает код и снижает риск ошибок синхронизации.
В отличие от FutureBuilder, который обрабатывает одно асинхронное значение, StreamBuilder предназначен для непрерывных потоков данных. FutureBuilder завершается после получения первого результата, тогда как StreamBuilder продолжает слушать поток и обновлять UI при каждом новом событии.
StreamBuilder применяется во всех сценариях, где данные поступают непрерывно: WebSocket-соединения, коллбеки датчиков, уведомления от Firebase, очереди событий Bluetooth и трансляции состояния приложения через BLoC. По данным анализа Flutter-проектов на GitHub (2025), StreamBuilder входит в тройку самых используемых Builder-виджетов наряду с FutureBuilder и LayoutBuilder.
Вывод: используйте StreamBuilder везде, где UI должен отражать непрерывно меняющиеся данные, избегая ручного управления состоянием через StatefulWidget.
StreamBuilder подписывается на Stream в момент сборки и отписывается при уничтожении виджета. Каждый раз, когда Stream испускает событие, StreamBuilder получает новый AsyncSnapshot и вызывает builder-функцию для перестроения UI.
Процесс состоит из трёх этапов. Первый: StreamBuilder создаёт подписку на переданный Stream через метод stream.listen. Второй: при каждом событии StreamBuilder обновляет внутренний AsyncSnapshot и помечает виджет как "грязный" для перестроения. Третий: фреймворк вызывает builder-функцию с новым snapshot, и UI отображает актуальные данные.
Важно: StreamBuilder использует StreamSubscription внутри себя. Если Stream передан напрямую, StreamBuilder подписывается один раз при инициализации. Если Stream меняется (например, при rebuild родителя), StreamBuilder отписывается от старого потока и подписывается на новый. Это поведение контролируется параметрами initialData и buildWhen, которые позволяют оптимизировать количество перестроений.
Вывод: понимание жизненного цикла подписки — основа правильного использования StreamBuilder. Неправильное управление стримами приводит к утечкам памяти или устаревшим данным в UI.
Свойство connectionState объекта AsyncSnapshot определяет, на каком этапе работы с потоком находится StreamBuilder. Различают четыре состояния: none, waiting, active, done.
None — начальное состояние, когда Stream ещё не начал передавать данные. В этом состоянии snapshot.connectionState равен ConnectionState.none, а snapshot.data равен null. Обычно в этом состоянии отображают заглушку или ожидание первого события. Если Stream не предоставляет начальных данных, StreamBuilder начинает с этого состояния.
Waiting — состояние ожидания данных от асинхронного потока. Stream активен, но данные ещё не поступили. Это состояние возникает, например, при загрузке данных из сети или при открытии долгоживущего соединения. В этом состоянии принято показывать CircularProgressIndicator или скелетон-загрузку.
Active — поток испускает данные, и UI отображает актуальную информацию. В этом состоянии snapshot.hasData равно true, а snapshot.data содержит последнее значение из потока. Если поток — Broadcast Stream, активное состояние может сосуществовать с ожиданием новых данных.
Done — поток завершён, новых данных не будет. Snapshot.data содержит последнее значение, переданное до закрытия стрима. Если поток успешно завершён, snapshot.hasError равен false. Это состояние используют для отображения финального результата: сообщение "Загрузка завершена" или переход к следующему экрану.
Вывод: при построении UI через StreamBuilder необходимо обрабатывать все четыре состояния, чтобы интерфейс корректно отображал загрузку, данные, ошибки и завершение.
StreamController — это класс из пакета dart:async, который создаёт и управляет Stream. StreamController позволяет добавлять данные, обрабатывать ошибки и закрывать поток, контролируя его жизненный цикл.
StreamController бывает двух типов: single-subscription (один подписчик) и broadcast (множество подписчиков). Single-subscription контроллер принимает только одного слушателя за раз — повторная подписка вызовет исключение. Broadcast контроллер позволяет нескольким StreamBuilder одновременно слушать один поток, что полезно для BLoC и общего состояния приложения.
При создании StreamController через StreamController<T>.broadcast() данные, добавленные до первой подписки, не воспроизводятся новым подписчикам. Если нужно получить последнее значение при подключении, используют BehaviourSubject из пакета rxdart, который кэширует последнее событие.
После завершения работы с контроллером необходимо вызвать controller.close(). Невызов close приводит к утечке ресурсов: поток остаётся открытым, подписчики продолжают висеть в памяти, и GC не освобождает связанные объекты.
Вывод: используйте StreamController с явным управлением жизненным циклом. Для single-subscription потоков — стандартный контроллер, для shared-состояния — broadcast контроллер или BehaviourSubject.
Пример 1 демонстрирует таймер с обратным отсчётом с использованием StreamController и StreamBuilder.
import 'dart:async';
class TimerWidget extends StatefulWidget {
const TimerWidget({super.key});
final StreamController<int> controller = StreamController<int>();
void startTimer() {
int count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
controller.sink.add(count++);
if (count > 10) {
controller.close();
timer.cancel();
}
});
}
}
В примере создаётся контроллер для генерации чисел от 0 до 10 с интервалом 1 секунда. После достижения 10 вызывается close, и поток завершается. StreamBuilder, подписанный на stream этого контроллера, будет отображать каждое новое значение.
Пример 2 — использование StreamBuilder с Broadcast Stream для отображения данных из нескольких источников.
final StreamController<String> broadcastController =
StreamController<String>.broadcast();
StreamBuilder<String>(
stream: broadcastController.stream,
initialData: 'Waiting for data...',
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return Text('Data: ${snapshot.data}');
},
)
Второй пример показывает обработку всех состояний: initialData для начального отображения, waiting для индикатора загрузки, hasError для ошибок и data для успешного результата. Такой паттерн — стандарт для production-кода с StreamBuilder.
Вывод: используйте initialData для избежания пустого экрана в первый момент и всегда обрабатывайте hasError для корректного отображения ошибок пользователю.
Ошибка 1: создание нового Stream при каждом rebuild родителя. Если Stream передаётся через выражение, которое создаёт новый объект при каждой сборке, StreamBuilder отписывается от старого и подписывается на новый поток, вызывая бесконечный цикл перестроений. Решение: использовать remembered переменную или StatefulWidget с фиксированным Stream.
Ошибка 2: отсутствие обработки ошибок. Stream может испускать ошибки через controller.sink.addError, и если builder не проверяет snapshot.hasError, пользователь видит пустой экран или бесконечную загрузку. Решение: всегда проверять hasError и отображать понятное сообщение.
Ошибка 3: утечка памяти из-за незакрытого StreamController. Если контроллер не закрыт в dispose, поток продолжает существовать, и GC не освобождает память. Решение: вызывать controller.close() в dispose и слушать done-событие для завершающих действий.
Ошибка 4: использование StreamBuilder с медленной builder-функцией. Поскольку builder вызывается при каждом событии потока, тяжёлые вычисления внутри него приводят к пропуску кадров. Решение: выносить вычисления в отдельный изолят или использовать Stream.map для трансформации данных.
Вывод: StreamBuilder — мощный, но требовательный инструмент. Следите за жизненным циклом Stream, обрабатывайте ошибки и избегайте тяжёлых операций в builder.
Часто задаваемые вопросы
FutureBuilder предназначен для однократного асинхронного результата: он подписывается на Future, получает одно значение и завершает работу. StreamBuilder подписывается на Stream, который может испускать множество значений во времени, и перестраивает UI при каждом новом событии.
AsyncSnapshot — неизменяемый объект, который содержит текущее состояние подписки (connectionState), последнее полученное значение (data) и объект ошибки (error), если поток испустил исключение.
Ошибка обрабатывается через свойство snapshot.hasError и snapshot.error в builder-функции. Если поток испускает ошибку методом sink.addError, AsyncSnapshot получает error, и builder должен отобразить соответствующее сообщение или fallback UI.
Да, если Stream — broadcast (создан через StreamController.broadcast). Single-subscription Stream допускает только одного подписчика. Для разделения одного потока между несколькими виджетами используйте broadcast контроллер или пакет rxdart с BehaviourSubject.
Используйте параметр buildWhen для фильтрации событий, при которых нужно перестраивать UI. Также применяйте Stream.transformer или Stream.where для фильтрации данных до передачи в StreamBuilder.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также