Flutter: Real-Time Data Streams with StreamBuilder
personAhmet Balaman
calendar_today
FlutterStreamBuilderStreamAsyncRealtimeWidget
StreamBuilder is a widget used in Flutter to listen to continuous data streams and update the UI accordingly. While FutureBuilder is used for one-time asynchronous operations, StreamBuilder is ideal for continuously flowing data.
Stream vs Future Difference
| Feature | Future | Stream |
|---|---|---|
| Data count | Single value | Multiple values |
| Usage | One-time operations | Continuous data flow |
| Example | HTTP request | WebSocket, Firebase |
| Widget | FutureBuilder | StreamBuilder |
Live Demo
You can try this widget in the interactive example below:
💡 If the example above doesn't load, click DartPad to run it in a new tab.
Basic Usage
StreamBuilder<int>(
stream: myStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
return Text('Data: ${snapshot.data}');
}
return Text('Waiting for data...');
},
)Important Properties
| Property | Description |
|---|---|
stream |
The Stream object to listen to |
builder |
Function that builds the UI |
initialData |
Initial data value |
ConnectionState States
| State | Description |
|---|---|
none |
Stream not yet connected |
waiting |
Stream connected, waiting for data |
active |
Stream active, receiving data |
done |
Stream closed |
Simple Counter Stream Example
class CounterStreamPage extends StatefulWidget {
@override
_CounterStreamPageState createState() => _CounterStreamPageState();
}
class _CounterStreamPageState extends State<CounterStreamPage> {
// Create a stream that increments every second
Stream<int> get counterStream async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // Send value to stream
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter Stream')),
body: Center(
child: StreamBuilder<int>(
stream: counterStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Text('Starting...');
}
if (snapshot.connectionState == ConnectionState.done) {
return Text('Completed! Final value: ${snapshot.data}');
}
return Text(
'${snapshot.data}',
style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
);
},
),
),
);
}
}Custom Stream with StreamController
You can create and control your own streams using StreamController:
class MessageStreamPage extends StatefulWidget {
@override
_MessageStreamPageState createState() => _MessageStreamPageState();
}
class _MessageStreamPageState extends State<MessageStreamPage> {
// Create StreamController
final StreamController<String> _messageController = StreamController<String>();
final List<String> _messages = [];
@override
void dispose() {
_messageController.close(); // Prevent memory leak
super.dispose();
}
void _addMessage(String message) {
_messageController.sink.add(message); // Add data to stream
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Message Stream')),
body: Column(
children: [
// Message sending buttons
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () => _addMessage('Hello!'),
child: Text('Hello'),
),
ElevatedButton(
onPressed: () => _addMessage('How are you?'),
child: Text('How are you?'),
),
ElevatedButton(
onPressed: () => _addMessage('Flutter is awesome!'),
child: Text('Flutter'),
),
],
),
SizedBox(height: 16),
// Stream listener
Expanded(
child: StreamBuilder<String>(
stream: _messageController.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
_messages.add(snapshot.data!);
}
if (_messages.isEmpty) {
return Center(child: Text('No messages yet'));
}
return ListView.builder(
itemCount: _messages.length,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.message),
title: Text(_messages[index]),
subtitle: Text('Message #${index + 1}'),
);
},
);
},
),
),
],
),
);
}
}Broadcast StreamController
Use broadcast stream for multiple listeners:
// Single listener (default)
final _controller = StreamController<int>();
// Multiple listeners
final _broadcastController = StreamController<int>.broadcast();
// You can listen in multiple places
StreamBuilder(stream: _broadcastController.stream, ...),
StreamBuilder(stream: _broadcastController.stream, ...),Stream Transformations
You can transform stream data using methods like map, where, expand:
Stream<int> numberStream = Stream.periodic(
Duration(seconds: 1),
(count) => count,
).take(10);
// Get only even numbers
Stream<int> evenNumbers = numberStream.where((n) => n % 2 == 0);
// Multiply each number by 2
Stream<int> doubledNumbers = numberStream.map((n) => n * 2);
// Convert to String
Stream<String> stringNumbers = numberStream.map((n) => 'Number: $n');
StreamBuilder<String>(
stream: stringNumbers,
builder: (context, snapshot) {
return Text(snapshot.data ?? 'Waiting...');
},
)Real World Example: Timer
class TimerPage extends StatefulWidget {
@override
_TimerPageState createState() => _TimerPageState();
}
class _TimerPageState extends State<TimerPage> {
late Stream<int> _timerStream;
bool _isRunning = false;
Stream<int> _createTimerStream() async* {
int seconds = 0;
while (true) {
await Future.delayed(Duration(seconds: 1));
seconds++;
yield seconds;
}
}
void _startTimer() {
setState(() {
_isRunning = true;
_timerStream = _createTimerStream();
});
}
String _formatTime(int totalSeconds) {
int minutes = totalSeconds ~/ 60;
int seconds = totalSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Timer')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isRunning)
StreamBuilder<int>(
stream: _timerStream,
builder: (context, snapshot) {
final time = snapshot.data ?? 0;
return Text(
_formatTime(time),
style: TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
);
},
)
else
Text(
'00:00',
style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
),
SizedBox(height: 32),
ElevatedButton.icon(
onPressed: _isRunning ? null : _startTimer,
icon: Icon(Icons.play_arrow),
label: Text('Start'),
),
],
),
),
);
}
}Usage with Firebase Firestore (Example)
// Listen to real-time data from Firestore
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('messages')
.orderBy('timestamp', descending: true)
.snapshots(), // Real-time stream
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Center(child: Text('No messages found'));
}
final messages = snapshot.data!.docs;
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index].data() as Map<String, dynamic>;
return ListTile(
title: Text(message['text']),
subtitle: Text(message['sender']),
);
},
);
},
)StreamBuilder vs FutureBuilder Comparison
// FutureBuilder - One-time data
FutureBuilder<User>(
future: fetchUser(), // Runs once
builder: (context, snapshot) {
// ...
},
)
// StreamBuilder - Continuous data flow
StreamBuilder<List<Message>>(
stream: messagesStream, // Continuously listens
builder: (context, snapshot) {
// Rebuilds every time new data arrives
},
)Best Practices
1. Dispose StreamController
@override
void dispose() {
_streamController.close(); // Prevent memory leak
super.dispose();
}2. Store Stream in State
class _MyPageState extends State<MyPage> {
late Stream<int> _myStream;
@override
void initState() {
super.initState();
_myStream = createStream(); // Create once
}
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: _myStream, // Use same stream
builder: (context, snapshot) {
// ...
},
);
}
}3. Use initialData
StreamBuilder<int>(
stream: counterStream,
initialData: 0, // Initial value
builder: (context, snapshot) {
// snapshot.data will never be null
return Text('${snapshot.data}');
},
)Summary
- StreamBuilder: For continuous data streams
- Stream: Asynchronous source that produces multiple values
- StreamController: Creating custom streams
- broadcast: Multiple listener support
- ConnectionState: Connection state control
- dispose: Close stream for memory management
StreamBuilder is an essential structure for all applications requiring real-time data such as Firebase, WebSocket, sensors, and more.