StreamBuilder — віджет Flutter, який автоматично перебудовує інтерфейс при отриманні нових даних з асинхронного потоку. На відміну від FutureBuilder, що працює з одноразовим результатом, StreamBuilder підтримує безперервне оновлення UI протягом усього життєвого циклу Stream. За даними офіційної документації Flutter (2026), StreamBuilder використовується в додатках реального часу: чати, стрічки новин, моніторинг датчиків і фінансові тікери. Це ключовий інструмент реактивного програмування, де UI відображає стан даних без ручних викликів setState.
Головне
StreamBuilder — це віджет з пакета Flutter SDK, який підписується на Stream та перебудовує свій дочірній елемент при кожній новій події потоку. StreamBuilder приймає об'єкт Stream і повертає віджет на основі останнього знімка, отриманого з потоку.
В архітектурі 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-функцію з новим знімком, і UI відображає актуальні дані.
Важливо: StreamBuilder використовує StreamSubscription всередині себе. Якщо Stream передано безпосередньо, StreamBuilder підписується один раз при ініціалізації. Якщо Stream змінюється (наприклад, при перебудові батька), 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('Помилка: ${snapshot.error}');
}
return Text('Дані: ${snapshot.data}');
},
)
Другий приклад показує обробку всіх станів: initialData для початкового відображення, waiting для індикатора завантаження, hasError для помилок та data для успішного результату. Такий патерн — стандарт для production-коду з StreamBuilder.
Висновок: використовуйте initialData для уникнення порожнього екрана в перший момент і завжди обробляйте hasError для коректного відображення помилок користувачеві.
Помилка 1: створення нового Stream при кожному перебудуванні батька. Якщо 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також