StreamBuilder è un widget Flutter che ricostruisce automaticamente l'interfaccia al ricevimento di nuovi dati da un flusso asincrono. A differenza di FutureBuilder, che lavora con un singolo risultato, StreamBuilder supporta aggiornamenti continui dell'UI per tutto il ciclo di vita dello Stream. Secondo la documentazione ufficiale di Flutter (2026), StreamBuilder viene utilizzato in applicazioni in tempo reale: chat, feed di notizie, monitoraggio di sensori e ticker finanziari. È uno strumento chiave della programmazione reattiva, dove l'UI riflette lo stato dei dati senza chiamate manuali a setState.
Punti chiave
StreamBuilder è un widget del pacchetto Flutter SDK che si sottoscrive a uno Stream e ricostruisce il suo elemento figlio a ogni nuovo evento del flusso. StreamBuilder accetta un oggetto Stream e restituisce un widget basato sull'ultimo snapshot ricevuto dal flusso.
Nell'architettura Flutter, StreamBuilder appartiene al gruppo dei widget Builder che separano la costruzione dell'UI dallo stato dei dati. A differenza di StatefulWidget, dove cambiare stato richiede una chiamata esplicita a setState, StreamBuilder reagisce agli eventi asincroni automaticamente, semplificando il codice e riducendo il rischio di errori di sincronizzazione.
A differenza di FutureBuilder, che gestisce un singolo valore asincrono, StreamBuilder è progettato per flussi di dati continui. FutureBuilder termina dopo aver ricevuto il primo risultato, mentre StreamBuilder continua ad ascoltare il flusso e ad aggiornare l'UI a ogni nuovo evento.
StreamBuilder viene utilizzato in tutti gli scenari in cui i dati arrivano continuamente: connessioni WebSocket, callback di sensori, notifiche Firebase, code di eventi Bluetooth e trasmissione dello stato dell'applicazione tramite BLoC. Secondo l'analisi dei progetti Flutter su GitHub (2025), StreamBuilder è tra i tre widget Builder più utilizzati insieme a FutureBuilder e LayoutBuilder.
Conclusione: utilizzate StreamBuilder ovunque l'UI debba riflettere dati in continuo cambiamento, evitando la gestione manuale dello stato tramite StatefulWidget.
StreamBuilder si sottoscrive a uno Stream al momento della costruzione e si disiscrive quando il widget viene distrutto. Ogni volta che lo Stream emette un evento, StreamBuilder riceve un nuovo AsyncSnapshot e chiama la funzione builder per ricostruire l'UI.
Il processo si compone di tre fasi. Prima: StreamBuilder crea una sottoscrizione allo Stream passato tramite il metodo stream.listen. Seconda: a ogni evento, StreamBuilder aggiorna l'AsyncSnapshot interno e marca il widget come sporco per la ricostruzione. Terza: il framework chiama la funzione builder con il nuovo snapshot e l'UI visualizza i dati correnti.
Importante: StreamBuilder utilizza StreamSubscription internamente. Se lo Stream viene passato direttamente, StreamBuilder si sottoscrive una volta durante l'inizializzazione. Se lo Stream cambia (ad esempio, durante una ricostruzione del genitore), StreamBuilder si disiscrive dal vecchio flusso e si sottoscrive al nuovo. Questo comportamento è controllato dai parametri initialData e buildWhen, che permettono di ottimizzare il numero di ricostruzioni.
Conclusione: comprendere il ciclo di vita della sottoscrizione è la base per un uso corretto di StreamBuilder. Una gestione errata dei flussi porta a perdite di memoria o dati obsoleti nell'UI.
La proprietà connectionState dell'oggetto AsyncSnapshot determina in quale fase di elaborazione del flusso si trova StreamBuilder. Ci sono quattro stati: none, waiting, active, done.
None è lo stato iniziale quando lo Stream non ha ancora iniziato a trasmettere dati. In questo stato, snapshot.connectionState è uguale a ConnectionState.none e snapshot.data è null. Di solito, in questo stato viene mostrato un placeholder o un indicatore di attesa. Se lo Stream non fornisce dati iniziali, StreamBuilder inizia in questo stato.
Waiting è lo stato di attesa di dati da un flusso asincrono. Lo Stream è attivo, ma i dati non sono ancora arrivati. Questo stato si verifica, ad esempio, durante il caricamento di dati dalla rete o l'apertura di una connessione di lunga durata. In questo stato, è comune mostrare un CircularProgressIndicator o uno scheletro di caricamento.
Active — il flusso emette dati e l'UI visualizza informazioni aggiornate. In questo stato, snapshot.hasData è true e snapshot.data contiene l'ultimo valore del flusso. Se il flusso è un Broadcast Stream, lo stato attivo può coesistere con l'attesa di nuovi dati.
Done — il flusso è terminato, non arriveranno nuovi dati. Snapshot.data contiene l'ultimo valore trasmesso prima della chiusura del flusso. Se il flusso è terminato con successo, snapshot.hasError è false. Questo stato viene utilizzato per visualizzare il risultato finale: un messaggio come “Caricamento completato” o una transizione alla schermata successiva.
Conclusione: quando si costruisce l'UI tramite StreamBuilder, tutti e quattro gli stati devono essere gestiti affinché l'interfaccia visualizzi correttamente caricamento, dati, errori e completamento.
StreamController è una classe del pacchetto dart:async che crea e gestisce uno Stream. StreamController permette di aggiungere dati, gestire errori e chiudere il flusso, controllandone il ciclo di vita.
StreamController è di due tipi: single-subscription (un abbonato) e broadcast (più abbonati). Un controller single-subscription accetta un solo ascoltatore alla volta — una seconda sottoscrizione lancerà un'eccezione. Un controller broadcast permette a più StreamBuilder di ascoltare lo stesso flusso simultaneamente, utile per BLoC e lo stato condiviso dell'applicazione.
Quando si crea uno StreamController tramite StreamController<T>.broadcast(), i dati aggiunti prima della prima sottoscrizione non vengono riprodotti ai nuovi abbonati. Per ottenere l'ultimo valore al momento della connessione, si utilizza BehaviourSubject del pacchetto rxdart, che memorizza nella cache l'ultimo evento.
Dopo aver terminato il lavoro con il controller, è necessario chiamare controller.close(). Non chiamare close porta a perdite di risorse: il flusso rimane aperto, gli abbonati restano in memoria e il GC non libera gli oggetti associati.
Conclusione: utilizzate StreamController con una gestione esplicita del ciclo di vita. Per flussi single-subscription, usate il controller standard; per stato condiviso, usate un controller broadcast o BehaviourSubject.
Esempio 1 mostra un timer di countdown utilizzando StreamController e 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();
}
});
}
}
Nell'esempio, viene creato un controller per generare numeri da 0 a 10 con intervallo di 1 secondo. Dopo aver raggiunto 10, viene chiamato close e il flusso termina. StreamBuilder, sottoscritto al flusso di questo controller, visualizzerà ogni nuovo valore.
Esempio 2 — utilizzo di StreamBuilder con un Broadcast Stream per visualizzare dati da più fonti.
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('Errore: ${snapshot.error}');
}
return Text('Dati: ${snapshot.data}');
},
)
Il secondo esempio mostra la gestione di tutti gli stati: initialData per la visualizzazione iniziale, waiting per l'indicatore di caricamento, hasError per gli errori e data per il risultato positivo. Questo pattern è lo standard per il codice di produzione con StreamBuilder.
Conclusione: utilizzate initialData per evitare una schermata vuota all'inizio e gestite sempre hasError per visualizzare correttamente gli errori all'utente.
Errore 1: creare un nuovo Stream a ogni ricostruzione del genitore. Se lo Stream viene passato attraverso un'espressione che crea un nuovo oggetto a ogni costruzione, StreamBuilder si disiscrive dal vecchio e si sottoscrive al nuovo flusso, causando un ciclo infinito di ricostruzioni. Soluzione: utilizzare una variabile remembered o un StatefulWidget con uno Stream fisso.
Errore 2: mancanza di gestione degli errori. Uno Stream può emettere errori tramite controller.sink.addError, e se il builder non verifica snapshot.hasError, l'utente vede una schermata vuota o un caricamento infinito. Soluzione: verificate sempre hasError e mostrate un messaggio chiaro.
Errore 3: perdite di memoria a causa di uno StreamController non chiuso. Se il controller non viene chiuso in dispose, il flusso continua a esistere e il GC non libera memoria. Soluzione: chiamate controller.close() in dispose e ascoltate l'evento done per le azioni finali.
Errore 4: utilizzo di StreamBuilder con una funzione builder lenta. Poiché il builder viene chiamato a ogni evento del flusso, calcoli pesanti al suo interno causano cadute di frame. Soluzione: spostate i calcoli in un isolate separato o utilizzate Stream.map per la trasformazione dei dati.
Conclusione: StreamBuilder è uno strumento potente ma esigente. Monitorate il ciclo di vita dello Stream, gestite gli errori ed evitate operazioni pesanti nel builder.
Domande frequenti
FutureBuilder è progettato per un singolo risultato asincrono: si sottoscrive a un Future, riceve un valore e termina. StreamBuilder si sottoscrive a uno Stream, che può emettere più valori nel tempo, e ricostruisce l'UI a ogni nuovo evento.
AsyncSnapshot è un oggetto immutabile che contiene lo stato corrente della sottoscrizione (connectionState), l'ultimo valore ricevuto (data) e un oggetto errore (error) se il flusso ha emesso un'eccezione.
Gli errori vengono gestiti tramite le proprietà snapshot.hasError e snapshot.error nella funzione builder. Se il flusso emette un errore tramite sink.addError, AsyncSnapshot riceve l'errore e il builder deve visualizzare un messaggio appropriato o un'UI di fallback.
Sì, se lo Stream è broadcast (creato tramite StreamController.broadcast). Uno Stream single-subscription permette un solo abbonato. Per condividere un flusso tra più widget, utilizzate un controller broadcast o il pacchetto rxdart con BehaviourSubject.
Utilizzate il parametro buildWhen per filtrare quali eventi devono attivare le ricostruzioni dell'UI. Applicate anche Stream.transformer o Stream.where per filtrare i dati prima di passarli a StreamBuilder.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche