StreamBuilder es un widget de Flutter que reconstruye automáticamente la interfaz al recibir nuevos datos de un flujo asíncrono. A diferencia de FutureBuilder, que trabaja con un único resultado, StreamBuilder admite actualizaciones continuas de la UI durante todo el ciclo de vida del Stream. Según la documentación oficial de Flutter (2026), StreamBuilder se utiliza en aplicaciones en tiempo real: chats, feeds de noticias, monitoreo de sensores y tickers financieros. Es una herramienta clave de la programación reactiva, donde la UI refleja el estado de los datos sin llamadas manuales a setState.
Puntos clave
StreamBuilder es un widget del paquete Flutter SDK que se suscribe a un Stream y reconstruye su elemento hijo en cada nuevo evento del flujo. StreamBuilder acepta un objeto Stream y devuelve un widget basado en el último snapshot recibido del flujo.
En la arquitectura de Flutter, StreamBuilder pertenece al grupo de widgets Builder que separan la construcción de la UI del estado de los datos. A diferencia de StatefulWidget, donde cambiar el estado requiere una llamada explícita a setState, StreamBuilder reacciona a eventos asíncronos automáticamente, simplificando el código y reduciendo el riesgo de errores de sincronización.
A diferencia de FutureBuilder, que maneja un único valor asíncrono, StreamBuilder está diseñado para flujos continuos de datos. FutureBuilder finaliza tras recibir el primer resultado, mientras que StreamBuilder sigue escuchando el flujo y actualizando la UI en cada nuevo evento.
StreamBuilder se utiliza en todos los escenarios donde los datos llegan de forma continua: conexiones WebSocket, callbacks de sensores, notificaciones de Firebase, colas de eventos Bluetooth y transmisión del estado de la aplicación a través de BLoC. Según el análisis de proyectos Flutter en GitHub (2025), StreamBuilder se encuentra entre los tres widgets Builder más utilizados, junto con FutureBuilder y LayoutBuilder.
Conclusión: use StreamBuilder siempre que la UI deba reflejar datos que cambian continuamente, evitando la gestión manual del estado mediante StatefulWidget.
StreamBuilder se suscribe a un Stream en el momento de la construcción y se da de baja cuando el widget se destruye. Cada vez que el Stream emite un evento, StreamBuilder recibe un nuevo AsyncSnapshot y llama a la función builder para reconstruir la UI.
El proceso consta de tres etapas. Primera: StreamBuilder crea una suscripción al Stream pasado mediante el método stream.listen. Segunda: en cada evento, StreamBuilder actualiza el AsyncSnapshot interno y marca el widget como sucio para su reconstrucción. Tercera: el framework llama a la función builder con el nuevo snapshot, y la UI muestra los datos actuales.
Importante: StreamBuilder utiliza StreamSubscription internamente. Si el Stream se pasa directamente, StreamBuilder se suscribe una vez durante la inicialización. Si el Stream cambia (por ejemplo, durante una reconstrucción del padre), StreamBuilder se da de baja del flujo antiguo y se suscribe al nuevo. Este comportamiento se controla mediante los parámetros initialData y buildWhen, que permiten optimizar el número de reconstrucciones.
Conclusión: comprender el ciclo de vida de la suscripción es la base para un uso correcto de StreamBuilder. Una gestión incorrecta de los flujos provoca fugas de memoria o datos obsoletos en la UI.
La propiedad connectionState del objeto AsyncSnapshot determina en qué etapa de procesamiento del flujo se encuentra StreamBuilder. Hay cuatro estados: none, waiting, active, done.
None es el estado inicial cuando el Stream aún no ha comenzado a transmitir datos. En este estado, snapshot.connectionState es igual a ConnectionState.none y snapshot.data es null. Normalmente se muestra un marcador de posición o un indicador de espera. Si el Stream no proporciona datos iniciales, StreamBuilder comienza en este estado.
Waiting es el estado de espera de datos de un flujo asíncrono. El Stream está activo, pero los datos aún no han llegado. Este estado se produce, por ejemplo, al cargar datos desde la red o al abrir una conexión de larga duración. En este estado es habitual mostrar un CircularProgressIndicator o un esqueleto de carga.
Active — el flujo emite datos y la UI muestra información actualizada. En este estado, snapshot.hasData es true y snapshot.data contiene el último valor del flujo. Si el flujo es un Broadcast Stream, el estado activo puede coexistir con la espera de nuevos datos.
Done — el flujo ha finalizado, no llegarán nuevos datos. Snapshot.data contiene el último valor transmitido antes del cierre del flujo. Si el flujo se completó correctamente, snapshot.hasError es false. Este estado se utiliza para mostrar el resultado final: un mensaje como “Carga completa” o una transición a la siguiente pantalla.
Conclusión: al construir la UI mediante StreamBuilder, deben manejarse los cuatro estados para que la interfaz muestre correctamente la carga, los datos, los errores y la finalización.
StreamController es una clase del paquete dart:async que crea y gestiona un Stream. StreamController permite añadir datos, manejar errores y cerrar el flujo, controlando su ciclo de vida.
StreamController tiene dos tipos: single-subscription (un suscriptor) y broadcast (múltiples suscriptores). Un controlador single-subscription acepta solo un oyente a la vez — una segunda suscripción lanzará una excepción. Un controlador broadcast permite que varios StreamBuilder escuchen el mismo flujo simultáneamente, lo que es útil para BLoC y el estado compartido de la aplicación.
Al crear un StreamController mediante StreamController<T>.broadcast(), los datos añadidos antes de la primera suscripción no se reproducen a los nuevos suscriptores. Para obtener el último valor al conectarse, se utiliza BehaviourSubject del paquete rxdart, que almacena en caché el último evento.
Después de terminar de trabajar con el controlador, debe llamarse a controller.close(). No llamar a close provoca fugas de recursos: el flujo permanece abierto, los suscriptores siguen en memoria y el GC no libera los objetos asociados.
Conclusión: use StreamController con una gestión explícita del ciclo de vida. Para flujos single-subscription, use el controlador estándar; para estado compartido, use un controlador broadcast o BehaviourSubject.
Ejemplo 1 demuestra un temporizador de cuenta atrás usando StreamController y 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();
}
});
}
}
En el ejemplo, se crea un controlador para generar números del 0 al 10 con un intervalo de 1 segundo. Al llegar a 10, se llama a close y el flujo finaliza. StreamBuilder, suscrito al flujo de este controlador, mostrará cada nuevo valor.
Ejemplo 2 — uso de StreamBuilder con un Broadcast Stream para mostrar datos de múltiples fuentes.
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('Datos: ${snapshot.data}');
},
)
El segundo ejemplo muestra el manejo de todos los estados: initialData para la visualización inicial, waiting para el indicador de carga, hasError para errores y data para el resultado exitoso. Este patrón es el estándar para código de producción con StreamBuilder.
Conclusión: use initialData para evitar una pantalla vacía al inicio y maneje siempre hasError para mostrar correctamente los errores al usuario.
Error 1: crear un nuevo Stream en cada reconstrucción del padre. Si el Stream se pasa mediante una expresión que crea un nuevo objeto en cada construcción, StreamBuilder se da de baja del antiguo y se suscribe al nuevo, provocando un bucle infinito de reconstrucciones. Solución: usar una variable remembered o un StatefulWidget con un Stream fijo.
Error 2: falta de manejo de errores. Un Stream puede emitir errores mediante controller.sink.addError, y si el builder no verifica snapshot.hasError, el usuario ve una pantalla vacía o una carga infinita. Solución: verificar siempre hasError y mostrar un mensaje claro.
Error 3: fugas de memoria por un StreamController no cerrado. Si el controlador no se cierra en dispose, el flujo sigue existiendo y el GC no libera memoria. Solución: llamar a controller.close() en dispose y escuchar el evento done para acciones finales.
Error 4: usar StreamBuilder con una función builder lenta. Dado que el builder se llama en cada evento del flujo, los cálculos pesados dentro de él provocan pérdida de fotogramas. Solución: mover los cálculos a un isolate separado o usar Stream.map para la transformación de datos.
Conclusión: StreamBuilder es una herramienta potente pero exigente. Supervise el ciclo de vida del Stream, maneje los errores y evite operaciones pesadas en el builder.
Preguntas frecuentes
FutureBuilder está diseñado para un único resultado asíncrono: se suscribe a un Future, recibe un valor y finaliza. StreamBuilder se suscribe a un Stream, que puede emitir múltiples valores a lo largo del tiempo, y reconstruye la UI en cada nuevo evento.
AsyncSnapshot es un objeto inmutable que contiene el estado actual de la suscripción (connectionState), el último valor recibido (data) y un objeto de error (error) si el flujo emitió una excepción.
Los errores se manejan mediante las propiedades snapshot.hasError y snapshot.error en la función builder. Si el flujo emite un error a través de sink.addError, AsyncSnapshot recibe el error y el builder debe mostrar un mensaje adecuado o una UI de respaldo.
Sí, si el Stream es broadcast (creado mediante StreamController.broadcast). Un Stream single-subscription permite solo un suscriptor. Para compartir un mismo flujo entre varios widgets, use un controlador broadcast o el paquete rxdart con BehaviourSubject.
Use el parámetro buildWhen para filtrar qué eventos deben desencadenar reconstrucciones de la UI. También puede aplicar Stream.transformer o Stream.where para filtrar los datos antes de pasarlos a StreamBuilder.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también