Ahmet Balaman LogoAhmet Balaman

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.

Yorumlar