Ahmet Balaman LogoAhmet Balaman

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.

Comments