Flutter: StreamBuilder ile Gerçek Zamanlı Veri Akışı
personAhmet Balaman
calendar_today
FlutterStreamBuilderStreamAsyncRealtimeWidget
StreamBuilder, Flutter'da sürekli veri akışlarını (streams) dinlemek ve UI'ı güncellemek için kullanılan widget'tır. FutureBuilder tek seferlik asenkron işlemler için kullanılırken, StreamBuilder sürekli akan veriler için idealdir.
Stream vs Future Farkı
| Özellik | Future | Stream |
|---|---|---|
| Veri sayısı | Tek değer | Birden fazla değer |
| Kullanım | Tek seferlik işlemler | Sürekli veri akışı |
| Örnek | HTTP isteği | WebSocket, Firebase |
| Widget | FutureBuilder | StreamBuilder |
Canlı Demo
Aşağıdaki interaktif örnekte bu widget'ı deneyebilirsiniz:
💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.
Temel Kullanım
StreamBuilder<int>(
stream: myStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Hata: ${snapshot.error}');
} else if (snapshot.hasData) {
return Text('Veri: ${snapshot.data}');
}
return Text('Veri bekleniyor...');
},
)Önemli Özellikler
| Özellik | Açıklama |
|---|---|
stream |
Dinlenecek Stream nesnesi |
builder |
UI oluşturan fonksiyon |
initialData |
Başlangıç verisi |
ConnectionState Durumları
| Durum | Açıklama |
|---|---|
none |
Stream henüz bağlanmadı |
waiting |
Stream bağlandı, veri bekleniyor |
active |
Stream aktif, veri alınıyor |
done |
Stream kapandı |
Basit Sayaç Stream Örneği
class CounterStreamPage extends StatefulWidget {
@override
_CounterStreamPageState createState() => _CounterStreamPageState();
}
class _CounterStreamPageState extends State<CounterStreamPage> {
// Her saniye artan bir stream oluştur
Stream<int> get counterStream async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // Stream'e değer gönder
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Sayaç Stream')),
body: Center(
child: StreamBuilder<int>(
stream: counterStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Text('Başlıyor...');
}
if (snapshot.connectionState == ConnectionState.done) {
return Text('Tamamlandı! Son değer: ${snapshot.data}');
}
return Text(
'${snapshot.data}',
style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
);
},
),
),
);
}
}StreamController ile Custom Stream
StreamController kullanarak kendi stream'lerinizi oluşturabilir ve kontrol edebilirsiniz:
class MessageStreamPage extends StatefulWidget {
@override
_MessageStreamPageState createState() => _MessageStreamPageState();
}
class _MessageStreamPageState extends State<MessageStreamPage> {
// StreamController oluştur
final StreamController<String> _messageController = StreamController<String>();
final List<String> _messages = [];
@override
void dispose() {
_messageController.close(); // Bellek sızıntısını önle
super.dispose();
}
void _addMessage(String message) {
_messageController.sink.add(message); // Stream'e veri ekle
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Mesaj Stream')),
body: Column(
children: [
// Mesaj gönderme butonları
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () => _addMessage('Merhaba!'),
child: Text('Merhaba'),
),
ElevatedButton(
onPressed: () => _addMessage('Nasılsın?'),
child: Text('Nasılsın?'),
),
ElevatedButton(
onPressed: () => _addMessage('Flutter harika!'),
child: Text('Flutter'),
),
],
),
SizedBox(height: 16),
// Stream dinleyici
Expanded(
child: StreamBuilder<String>(
stream: _messageController.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
_messages.add(snapshot.data!);
}
if (_messages.isEmpty) {
return Center(child: Text('Henüz mesaj yok'));
}
return ListView.builder(
itemCount: _messages.length,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.message),
title: Text(_messages[index]),
subtitle: Text('Mesaj #${index + 1}'),
);
},
);
},
),
),
],
),
);
}
}Broadcast StreamController
Birden fazla dinleyici için broadcast stream kullanın:
// Tek dinleyici (varsayılan)
final _controller = StreamController<int>();
// Çoklu dinleyici
final _broadcastController = StreamController<int>.broadcast();
// Birden fazla yerde dinleyebilirsiniz
StreamBuilder(stream: _broadcastController.stream, ...),
StreamBuilder(stream: _broadcastController.stream, ...),Stream Dönüşümleri
Stream verilerini dönüştürmek için map, where, expand gibi metodlar kullanabilirsiniz:
Stream<int> numberStream = Stream.periodic(
Duration(seconds: 1),
(count) => count,
).take(10);
// Sadece çift sayıları al
Stream<int> evenNumbers = numberStream.where((n) => n % 2 == 0);
// Her sayıyı 2 ile çarp
Stream<int> doubledNumbers = numberStream.map((n) => n * 2);
// String'e dönüştür
Stream<String> stringNumbers = numberStream.map((n) => 'Sayı: $n');
StreamBuilder<String>(
stream: stringNumbers,
builder: (context, snapshot) {
return Text(snapshot.data ?? 'Bekleniyor...');
},
)Gerçek Dünya Örneği: Zamanlayıcı (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('Zamanlayıcı')),
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('Başlat'),
),
],
),
),
);
}
}Firebase Firestore ile Kullanım (Örnek)
// Firestore'dan gerçek zamanlı veri dinleme
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('messages')
.orderBy('timestamp', descending: true)
.snapshots(), // Gerçek zamanlı stream
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Hata: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Center(child: Text('Mesaj bulunamadı'));
}
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 Karşılaştırma
// FutureBuilder - Tek seferlik veri
FutureBuilder<User>(
future: fetchUser(), // Bir kez çalışır
builder: (context, snapshot) {
// ...
},
)
// StreamBuilder - Sürekli veri akışı
StreamBuilder<List<Message>>(
stream: messagesStream, // Sürekli dinler
builder: (context, snapshot) {
// Her yeni veri geldiğinde rebuild olur
},
)Best Practices
1. StreamController'ı dispose edin
@override
void dispose() {
_streamController.close(); // Bellek sızıntısını önle
super.dispose();
}2. Stream'i state'te saklayın
class _MyPageState extends State<MyPage> {
late Stream<int> _myStream;
@override
void initState() {
super.initState();
_myStream = createStream(); // Bir kez oluştur
}
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: _myStream, // Aynı stream'i kullan
builder: (context, snapshot) {
// ...
},
);
}
}3. initialData kullanın
StreamBuilder<int>(
stream: counterStream,
initialData: 0, // İlk değer
builder: (context, snapshot) {
// snapshot.data hiçbir zaman null olmaz
return Text('${snapshot.data}');
},
)Özet
- StreamBuilder: Sürekli veri akışları için
- Stream: Birden fazla değer üreten asenkron kaynak
- StreamController: Custom stream oluşturma
- broadcast: Çoklu dinleyici desteği
- ConnectionState: Bağlantı durumu kontrolü
- dispose: Bellek yönetimi için stream'i kapatma
StreamBuilder, Firebase, WebSocket, sensörler ve gerçek zamanlı veri gerektiren tüm uygulamalar için vazgeçilmez bir yapıdır.