StreamBuilder: co to jest, zasada działania i zastosowanie we Flutter

Autor: IT Sectr Opublikowano: 2026-07-03 Czas czytania: 8 min

StreamBuilder — widget Flutter, który automatycznie przebudowuje interfejs po otrzymaniu nowych danych z asynchronicznego strumienia. W przeciwieństwie do FutureBuilder, który działa z jednorazowym wynikiem, StreamBuilder obsługuje ciągłą aktualizację UI przez cały cykl życia Stream. Według oficjalnej dokumentacji Flutter (2026), StreamBuilder jest używany w aplikacjach czasu rzeczywistego: czaty, kanały informacyjne, monitorowanie czujników i tickery finansowe. To kluczowe narzędzie programowania reaktywnego, gdzie UI odzwierciedla stan danych bez ręcznych wywołań setState.

Najważniejsze

  • StreamBuilder — widget przyjmujący Stream i snapshot danych do reaktywnego renderowania UI
  • Snapshot zawiera connectionState, data i error, określające bieżący stan strumienia
  • ConnectionState przechodzi cztery fazy: none, waiting, active, done
  • AsyncSnapshot — niezmienny obiekt, który gwarantuje spójność danych na każdej klatce
  • StreamController zarządza strumieniem: dodaje dane, obsługuje błędy i zamyka Stream

Co to jest StreamBuilder

StreamBuilder — to widget z pakietu Flutter SDK, który subskrybuje się do Stream i przebudowuje swój element potomny przy każdym nowym zdarzeniu strumienia. StreamBuilder przyjmuje obiekt Stream i zwraca widget na podstawie ostatniego snapshot uzyskanego ze strumienia.

W architekturze Flutter StreamBuilder należy do grupy Builder-widgetów, które oddzielają budowę UI od stanu danych. W przeciwieństwie do StatefulWidget, gdzie zmiana stanu wymaga jawnego wywołania setState, StreamBuilder reaguje na zdarzenia asynchroniczne automatycznie, co upraszcza kod i zmniejsza ryzyko błędów synchronizacji.

W przeciwieństwie do FutureBuilder, który obsługuje pojedynczą wartość asynchroniczną, StreamBuilder jest przeznaczony dla ciągłych strumieni danych. FutureBuilder kończy się po otrzymaniu pierwszego wyniku, podczas gdy StreamBuilder kontynuuje nasłuchiwanie strumienia i aktualizuje UI przy każdym nowym zdarzeniu.

StreamBuilder jest używany we wszystkich scenariuszach, gdzie dane napływają w sposób ciągły: połączenia WebSocket, callbacki czujników, powiadomienia z Firebase, kolejki zdarzeń Bluetooth i transmisje stanu aplikacji przez BLoC. Według analizy projektów Flutter na GitHub (2025), StreamBuilder wchodzi do trójki najczęściej używanych Builder-widgetów obok FutureBuilder i LayoutBuilder.

Podsumowanie: używaj StreamBuilder wszędzie tam, gdzie UI musi odzwierciedlać stale zmieniające się dane, unikając ręcznego zarządzania stanem przez StatefulWidget.

Jak działa StreamBuilder

StreamBuilder subskrybuje się do Stream w momencie budowy i anuluje subskrypcję przy zniszczeniu widgeta. Za każdym razem, gdy Stream emituje zdarzenie, StreamBuilder otrzymuje nowy AsyncSnapshot i wywołuje funkcję builder do przebudowania UI.

Proces składa się z trzech etapów. Pierwszy: StreamBuilder tworzy subskrypcję na przekazany Stream przez metodę stream.listen. Drugi: przy każdym zdarzeniu StreamBuilder aktualizuje wewnętrzny AsyncSnapshot i oznacza widget jako „brudny“ do przebudowy. Trzeci: framework wywołuje funkcję builder z nowym snapshot, a UI wyświetla aktualne dane.

Ważne: StreamBuilder używa StreamSubscription wewnętrznie. Jeśli Stream jest przekazany bezpośrednio, StreamBuilder subskrybuje się raz przy inicjalizacji. Jeśli Stream się zmienia (np. przy rebuild rodzica), StreamBuilder anuluje subskrypcję starego strumienia i subskrybuje nowy. To zachowanie jest kontrolowane przez parametry initialData i buildWhen, które pozwalają zoptymalizować liczbę przebudów.

Podsumowanie: zrozumienie cyklu życia subskrypcji jest podstawą prawidłowego użycia StreamBuilder. Niewłaściwe zarządzanie strumieniami prowadzi do wycieków pamięci lub nieaktualnych danych w UI.

ConnectionState: cztery stany strumienia

Właściwość connectionState obiektu AsyncSnapshot określa, na którym etapie pracy ze strumieniem znajduje się StreamBuilder. Wyróżnia się cztery stany: none, waiting, active, done.

ConnectionState.none

None — stan początkowy, gdy Stream jeszcze nie zaczął przekazywać danych. W tym stanie snapshot.connectionState jest równy ConnectionState.none, a snapshot.data ma wartość null. Zazwyczaj w tym stanie wyświetla się placeholder lub oczekiwanie na pierwsze zdarzenie. Jeśli Stream nie dostarcza danych początkowych, StreamBuilder zaczyna od tego stanu.

ConnectionState.waiting

Waiting — stan oczekiwania na dane z asynchronicznego strumienia. Stream jest aktywny, ale dane jeszcze nie nadeszły. Ten stan występuje na przykład podczas ładowania danych z sieci lub otwierania długotrwałego połączenia. W tym stanie zwykle wyświetla się CircularProgressIndicator lub szkielet ładowania.

ConnectionState.active

Active — strumień emituje dane, a UI wyświetla aktualną informację. W tym stanie snapshot.hasData ma wartość true, a snapshot.data zawiera ostatnią wartość ze strumienia. Jeśli Stream to Broadcast Stream, stan aktywny może współistnieć z oczekiwaniem na nowe dane.

ConnectionState.done

Done — strumień jest zakończony, nowych danych nie będzie. Snapshot.data zawiera ostatnią wartość przekazaną przed zamknięciem strumienia. Jeśli strumień zakończył się pomyślnie, snapshot.hasError ma wartość false. Ten stan jest używany do wyświetlenia wyniku końcowego: komunikat „Ładowanie zakończone“ lub przejście do następnego ekranu.

Podsumowanie: przy budowie UI przez StreamBuilder należy obsłużyć wszystkie cztery stany, aby interfejs poprawnie wyświetlał ładowanie, dane, błędy i zakończenie.

Użycie StreamController do zarządzania strumieniem

StreamController — to klasa z pakietu dart:async, która tworzy i zarządza Stream. StreamController pozwala dodawać dane, obsługiwać błędy i zamykać strumień, kontrolując jego cykl życia.

StreamController występuje w dwóch typach: single-subscription (jeden subskrybent) i broadcast (wielu subskrybentów). Kontroler single-subscription przyjmuje tylko jednego słuchacza na raz — ponowna subskrypcja spowoduje wyjątek. Kontroler broadcast pozwala kilku StreamBuilder jednocześnie nasłuchiwać jednego strumienia, co jest przydatne dla BLoC i wspólnego stanu aplikacji.

Podczas tworzenia StreamController przez StreamController<T>.broadcast() dane dodane przed pierwszą subskrypcją nie są odtwarzane nowemu subskrybentowi. Jeśli potrzebujesz uzyskać ostatnią wartość przy podłączeniu, użyj BehaviourSubject z pakietu rxdart, który buforuje ostatnie zdarzenie.

Po zakończeniu pracy z kontrolerem należy wywołać controller.close(). Niewywołanie close prowadzi do wycieku zasobów: strumień pozostaje otwarty, subskrybenci nadal wiszą w pamięci, a GC nie zwalnia powiązanych obiektów.

Podsumowanie: używaj StreamController z jawnym zarządzaniem cyklem życia. Dla strumieni single-subscription — standardowy kontroler, dla stanu współdzielonego — kontroler broadcast lub BehaviourSubject.

Przykłady kodu z StreamBuilder

Przykład 1 demonstruje timer z odliczaniem wstecznym przy użyciu StreamController i StreamBuilder.

dart
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();
      }
    });
  }
}

W przykładzie tworzony jest kontroler do generowania liczb od 0 do 10 w interwale 1 sekundy. Po osiągnięciu 10 wywoływane jest close, a strumień kończy się. StreamBuilder, subskrybowany do stream tego kontrolera, będzie wyświetlał każdą nową wartość.

Przykład 2 — użycie StreamBuilder z Broadcast Stream do wyświetlania danych z wielu źródeł.

dart
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}');
  },
)

Drugi przykład pokazuje obsługę wszystkich stanów: initialData do początkowego wyświetlenia, waiting dla wskaźnika ładowania, hasError dla błędów i data dla pomyślnego wyniku. Taki wzorzec jest standardem w kodzie produkcyjnym z StreamBuilder.

Podsumowanie: używaj initialData, aby uniknąć pustego ekranu w pierwszej chwili i zawsze obsługuj hasError dla poprawnego wyświetlania błędów użytkownikowi.

Typowe błędy przy pracy z StreamBuilder

Błąd 1: tworzenie nowego Stream przy każdym rebuild rodzica. Jeśli Stream jest przekazywany przez wyrażenie, które tworzy nowy obiekt przy każdej budowie, StreamBuilder anuluje subskrypcję starego i subskrybuje nowy strumień, powodując nieskończoną pętlę przebudów. Rozwiązanie: użyj zmiennej remembered lub StatefulWidget z ustalonym Stream.

Błąd 2: brak obsługi błędów. Stream może emitować błędy przez controller.sink.addError, a jeśli builder nie sprawdza snapshot.hasError, użytkownik widzi pusty ekran lub nieskończone ładowanie. Rozwiązanie: zawsze sprawdzaj hasError i wyświetlaj zrozumiały komunikat.

Błąd 3: wyciek pamięci z powodu niezamkniętego StreamController. Jeśli kontroler nie jest zamknięty w dispose, strumień nadal istnieje, a GC nie zwalnia pamięci. Rozwiązanie: wywołuj controller.close() w dispose i nasłuchuj zdarzenia done dla czynności końcowych.

Błąd 4: używanie StreamBuilder z wolną funkcją builder. Ponieważ builder jest wywoływany przy każdym zdarzeniu strumienia, ciężkie obliczenia wewnątrz niego prowadzą do pomijania klatek. Rozwiązanie: przenieś obliczenia do osobnego izolatu lub użyj Stream.map do transformacji danych.

Podsumowanie: StreamBuilder — potężne, ale wymagające narzędzie. Zwracaj uwagę na cykl życia Stream, obsługuj błędy i unikaj ciężkich operacji w builder.

Często zadawane pytania

Czym StreamBuilder różni się od FutureBuilder?

FutureBuilder jest przeznaczony dla jednorazowego wyniku asynchronicznego: subskrybuje się do Future, otrzymuje jedną wartość i kończy pracę. StreamBuilder subskrybuje się do Stream, który może emitować wiele wartości w czasie, i przebudowuje UI przy każdym nowym zdarzeniu.

Czym jest AsyncSnapshot w StreamBuilder?

AsyncSnapshot — niezmienny obiekt, który zawiera bieżący stan subskrypcji (connectionState), ostatnią otrzymaną wartość (data) oraz obiekt błędu (error), jeśli strumień wyemitował wyjątek.

Jak obsłużyć błąd w StreamBuilder?

Błąd jest obsługiwany przez właściwości snapshot.hasError i snapshot.error w funkcji builder. Jeśli strumień emituje błąd metodą sink.addError, AsyncSnapshot otrzymuje error, a builder musi wyświetlić odpowiedni komunikat lub fallback UI.

Czy można użyć jednego Stream w kilku StreamBuilder?

Tak, jeśli Stream jest broadcast (utworzony przez StreamController.broadcast). Single-subscription Stream dopuszcza tylko jednego subskrybenta. Do współdzielenia jednego strumienia między wieloma widgetami używaj kontrolera broadcast lub pakietu rxdart z BehaviourSubject.

Jak uniknąć przebudowy StreamBuilder przy każdym zdarzeniu?

Użyj parametru buildWhen do filtrowania zdarzeń, przy których należy przebudować UI. Stosuj również Stream.transformer lub Stream.where do filtrowania danych przed przekazaniem do StreamBuilder.

Podsumowanie

  • StreamBuilder — widget do reaktywnego budowania UI na podstawie asynchronicznego strumienia danych, obsługujący ciągłą aktualizację interfejsu
  • AsyncSnapshot zawiera connectionState (none, waiting, active, done), data i error — wszystkie stany strumienia
  • StreamController zarządza cyklem życia strumienia: dodawanie danych, obsługa błędów i zamykanie strumienia
  • Broadcast Stream pozwala kilku StreamBuilder subskrybować jeden strumień, single-subscription — tylko jednemu
  • Obsługa błędów jest obowiązkowa: bez sprawdzania hasError aplikacja może zawisnąć w stanie ładowania
  • Wyciek pamięci — najczęstszy problem: zawsze zamykaj StreamController w dispose
  • Zalecenie: zawsze podawaj initialData i obsługuj wszystkie cztery connectionState dla płynnego UX

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również