Flutter: Echtzeit-Datenströme mit StreamBuilder
Ahmet Balaman
FlutterStreamBuilderStreamAsyncRealtimeWidget
StreamBuilder ist ein Widget in Flutter, das kontinuierliche Datenströme abhört und die Oberfläche entsprechend aktualisiert. Während FutureBuilder für einmalige asynchrone Vorgänge gedacht ist, eignet sich StreamBuilder für fortlaufend eintreffende Daten.
Unterschied zwischen Stream und Future
| Merkmal | Future | Stream |
|---|---|---|
| Anzahl der Daten | Ein Wert | Mehrere Werte |
| Einsatz | Einmalige Vorgänge | Fortlaufender Datenfluss |
| Beispiel | HTTP-Anfrage | WebSocket, Firebase |
| Widget | FutureBuilder | StreamBuilder |
Live-Demo
Sie können dieses Widget im interaktiven Beispiel unten ausprobieren:
💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.
Grundlegende Verwendung
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...');
},
)Wichtige Eigenschaften
| Eigenschaft | Beschreibung |
|---|---|
stream |
Das Stream-Objekt, das abgehört wird |
builder |
Funktion, die die Oberfläche aufbaut |
initialData |
Anfangswert der Daten |
Zustände von ConnectionState
| Zustand | Beschreibung |
|---|---|
none |
Der Stream ist noch nicht verbunden |
waiting |
Der Stream ist verbunden und wartet auf Daten |
active |
Der Stream ist aktiv und empfängt Daten |
done |
Der Stream wurde geschlossen |
Einfaches Beispiel: Zähler-Stream
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),
);
},
),
),
);
}
}Eigener Stream mit StreamController
Mit StreamController können Sie eigene Streams erzeugen und steuern:
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
Für mehrere Zuhörer verwenden Sie einen broadcast-Stream:
// 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, ...),Streams transformieren
Mit Methoden wie map, where und expand können Sie Stream-Daten umformen:
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...');
},
)Praxisbeispiel: 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'),
),
],
),
),
);
}
}Verwendung mit Firebase Firestore (Beispiel)
// 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']),
);
},
);
},
)Vergleich: StreamBuilder und FutureBuilder
// 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. StreamController schließen
@override
void dispose() {
_streamController.close(); // Prevent memory leak
super.dispose();
}2. Den Stream im State ablegen
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. initialData verwenden
StreamBuilder<int>(
stream: counterStream,
initialData: 0, // Initial value
builder: (context, snapshot) {
// snapshot.data will never be null
return Text('${snapshot.data}');
},
)Zusammenfassung
- StreamBuilder: Für fortlaufende Datenströme
- Stream: Asynchrone Quelle, die mehrere Werte liefert
- StreamController: Zum Erzeugen eigener Streams
- broadcast: Unterstützt mehrere Zuhörer
- ConnectionState: Kontrolle des Verbindungszustands
- dispose: Stream schließen, um Speicher freizugeben
StreamBuilder ist unverzichtbar für alle Anwendungen mit Echtzeitdaten, etwa Firebase, WebSocket oder Sensoren.