İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

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.

Kommentare