Flutter Lesson 45 of 83 3 min read
StreamBuilder in Flutter
Learn StreamBuilder in Flutter: rebuild UI for every stream event, handle connection states, and use it for timers, sockets and live data.
On this page
A FutureBuilder handles one value that arrives later. A StreamBuilder handles many: it rebuilds every time the stream emits. Use it for anything live, such as a timer, a chat, a download’s progress or a database that pushes changes.
If streams are new to you, read the Dart lesson on streams first.
A live clock #
import 'package:flutter/material.dart';
Stream<DateTime> clock() async* {
while (true) {
yield DateTime.now();
await Future<void>.delayed(const Duration(seconds: 1));
}
}
void main() => runApp(const MaterialApp(home: ClockPage()));
class ClockPage extends StatefulWidget {
const ClockPage({super.key});
@override
State<ClockPage> createState() => _ClockPageState();
}
class _ClockPageState extends State<ClockPage> {
late final Stream<DateTime> _clock = clock(); // created once
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<DateTime>(
stream: _clock,
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
final t = snapshot.data!;
String two(int n) => n.toString().padLeft(2, '0');
return Text(
'${two(t.hour)}:${two(t.minute)}:${two(t.second)}',
style: const TextStyle(fontSize: 56),
);
},
),
),
);
}
}
StreamBuilder subscribes when it appears and cancels the subscription when it is removed. You do not manage the subscription yourself.
Connection states #
connectionState | Meaning |
|---|---|
none | No stream was given |
waiting | Subscribed, nothing received yet |
active | At least one event has arrived, and more may come |
done | The stream has closed |
builder: (context, snapshot) {
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (!snapshot.hasData) return const Text('No data');
return Text('${snapshot.data}');
}
initialData #
If you know a sensible starting value, pass it and skip the waiting state.
StreamBuilder<int>(
stream: _unreadCount,
initialData: 0,
builder: (context, snapshot) => Badge(
label: Text('${snapshot.data}'),
child: const Icon(Icons.mail),
),
)
The same mistake as FutureBuilder #
Do not create the stream inside build. Each rebuild would create a new stream and resubscribe, and for a network connection that means reconnecting.
// Wrong
StreamBuilder(stream: clock(), builder: ...)
// Right: create once in the State, then pass the field
StreamBuilder(stream: _clock, builder: ...)
Your own stream of events #
A StreamController lets you push values in from your own code.
import 'dart:async';
class DownloadService {
final _progress = StreamController<double>.broadcast();
Stream<double> get progress => _progress.stream;
Future<void> download() async {
for (var i = 0; i <= 100; i += 5) {
await Future<void>.delayed(const Duration(milliseconds: 150));
_progress.add(i / 100);
}
}
void dispose() => _progress.close();
}
StreamBuilder<double>(
stream: _service.progress,
initialData: 0,
builder: (context, snapshot) {
final value = snapshot.data!;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(value: value),
const SizedBox(height: 8),
Text('${(value * 100).round()}%'),
],
);
},
)
Where streams come from in real apps #
| Source | Stream of |
|---|---|
Firebase Firestore .snapshots() | The live contents of a collection or document |
Firebase Auth authStateChanges() | The current user, or null |
| A WebSocket channel | Messages from a server |
| Local databases such as Drift and Isar | Query results that update when data changes |
connectivity_plus | Online and offline changes |
| Sensors and location | Readings |
// Decide which screen to show based on login state.
StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen();
}
return snapshot.hasData ? const HomePage() : const LoginPage();
},
)
Errors do not end the builder #
When a stream emits an error, the snapshot carries it and the builder runs. Later events still arrive. Always handle hasError.
StreamBuilder or a state holder? #
StreamBuilder is fine for showing a stream directly. When the data needs processing, combining with other state, or sharing across screens, subscribe in a state holder and expose plain state. Riverpod’s StreamProvider and Bloc’s emit.forEach do this for you.
Try it yourself #
Build a countdown timer: a stream that emits the remaining seconds from 10 to 0, shown in large text, with a “Done!” message when the stream closes, and a button that restarts it by creating a new stream in setState.