flutter
/

Flutter Stream – Real‑Time Data and Event Handling

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Flutter Stream – Real‑Time Data and Event Handling

What is a Stream in Flutter?

A Stream is a sequence of asynchronous events. It’s like a pipe where data flows over time – you can listen to the stream and react whenever a new event arrives. Streams are essential for handling real‑time updates, user input, network responses, and any situation where data arrives continuously or at unpredictable times. In Flutter, the StreamBuilder widget makes it easy to build UI that automatically updates when the stream emits new data.

Creating a Stream

There are several ways to create a stream:

    • StreamController: Manually add events (useful for custom event sources).
    • async* functions: Generate events using yield (like a generator).
    • Stream.fromIterable: Create a stream from an existing collection.
    • Stream.periodic: Emit events at regular intervals.
    • Stream.fromFuture: Convert a single Future to a stream (only one event).

Using StreamController

The StreamController gives you a sink to add events and a stream to listen to. You can add data, errors, and close the stream when done.

DARTRead-only
1
final controller = StreamController<int>();

// Add events
controller.sink.add(1);
controller.sink.add(2);
controller.sink.addError('Oops');
controller.close(); // no more events

// Listen to the stream
controller.stream.listen(
  (data) => print('Data: $data'),
  onError: (error) => print('Error: $error'),
  onDone: () => print('Done'),
);

Remember to dispose of the controller when you no longer need it to avoid memory leaks.

async* Generator Functions

An async* function automatically returns a Stream. Inside, you use yield to emit values. This is ideal for generating sequences on the fly.

DARTRead-only
1
Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

void main() async {
  await for (int value in countStream(5)) {
    print(value); // prints 1,2,3,4,5 each second
  }
}

Listening to a Stream with StreamBuilder

StreamBuilder is a Flutter widget that listens to a stream and rebuilds whenever new data arrives. It’s perfect for updating UI in response to asynchronous events. It provides a snapshot containing the current connection state, data, and error.

DARTRead-only
1
StreamBuilder<int>(
  stream: myStream,
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }
    if (!snapshot.hasData) {
      return CircularProgressIndicator();
    }
    return Text('Value: ${snapshot.data}');
  },
)

The snapshot’s connectionState tells you if the stream is waiting, active, or done. You can use this to show loading states or a completion message.

Broadcast Streams

By default, streams are single‑subscription – only one listener can listen at a time, and listening again throws an error. Use a broadcast stream when you need multiple listeners. You can create one with StreamController.broadcast() or by calling .asBroadcastStream() on an existing stream.

DARTRead-only
1
final controller = StreamController<int>.broadcast();

controller.stream.listen((data) => print('Listener 1: $data'));
controller.stream.listen((data) => print('Listener 2: $data'));

controller.sink.add(42); // both listeners receive 42

Common Patterns

    • Debouncing: Use debounce from rxdart or manually implement using Timer to wait for a pause in events (e.g., search field).
    • Combining streams: Use StreamGroup.merge or rxdart to combine multiple streams into one.
    • Stream transformations: Use map, where, expand, take, skip to transform the data before it reaches the listener.
    • Error handling: Use handleError or the onError callback to catch errors gracefully.

StreamBuilder vs FutureBuilder

FutureBuilder handles a single asynchronous value (one Future). StreamBuilder handles a sequence of values over time. Use FutureBuilder for one‑time loads (like fetching a user profile), and StreamBuilder for continuous updates (like real‑time chat or live sensor data).

Best Practices

    • Always close StreamControllers in dispose() to avoid memory leaks.
    • Use StreamBuilder inside the widget tree, not in a separate async method.
    • Provide an initialData to StreamBuilder if you want to show something before the first event.
    • For performance, use const widgets inside the builder where possible.
    • Prefer async* generators for simple sequences; use StreamController for manual control.

Common Mistakes

    • Not disposing StreamController: Causes memory leaks and potential crashes.
    • Using a single‑subscription stream with multiple listeners: Throws an error; switch to broadcast.
    • Calling setState inside StreamBuilder: Unnecessary; the builder already rebuilds when new data arrives.
    • Creating the stream inside the build method: Causes the stream to be recreated on every rebuild, which can break subscriptions. Create the stream in initState or use a state management solution.

Complete Example

DARTRead-only
1
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Stream Demo')),
        body: Center(child: CounterWidget()),
      ),
    );
  }
}

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  final StreamController<int> _controller = StreamController<int>();
  int _count = 0;

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }

  void _increment() {
    _count++;
    _controller.sink.add(_count);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        StreamBuilder<int>(
          stream: _controller.stream,
          initialData: 0,
          builder: (context, snapshot) {
            return Text(
              'Count: ${snapshot.data}',
              style: TextStyle(fontSize: 24),
            );
          },
        ),
        ElevatedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Key Takeaways

    • Streams represent sequences of asynchronous events.
    • Use StreamController to manually add events, and async* for generators.
    • StreamBuilder rebuilds the UI when new data arrives.
    • Broadcast streams allow multiple listeners; single‑subscription streams allow only one.
    • Always dispose of StreamController to avoid leaks.
    • Streams are ideal for real‑time updates, user input, and any continuous data flow.

Try it yourself

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Timer Stream')),
        body: Center(child: TimerWidget()),
      ),
    );
  }
}

class TimerWidget extends StatefulWidget {
  @override
  _TimerWidgetState createState() => _TimerWidgetState();
}

class _TimerWidgetState extends State<TimerWidget> {
  Stream<int> _timerStream() async* {
    for (int i = 0; i <= 10; i++) {
      await Future.delayed(Duration(seconds: 1));
      yield i;
    }
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: _timerStream(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        if (!snapshot.hasData) {
          return Text('Timer started');
        }
        return Text(
          'Seconds elapsed: ${snapshot.data}',
          style: TextStyle(fontSize: 24),
        );
      },
    );
  }
}

Test Your Knowledge

Q1
of 4

What is the primary difference between a Future and a Stream?

A
A Future returns a value, a Stream does not
B
A Stream represents a sequence of values over time, a Future a single value
C
Futures are synchronous, Streams are asynchronous
D
There is no difference
Q2
of 4

Which widget is used to listen to a stream and rebuild the UI?

A
FutureBuilder
B
StreamBuilder
C
StreamListener
D
AsyncBuilder
Q3
of 4

How do you create a stream that can have multiple listeners?

A
Use a regular StreamController
B
Use a broadcast StreamController
C
Use a single‑subscription controller and add more listeners
D
Use a Future
Q4
of 4

What must you do with a StreamController to avoid memory leaks?

A
Call `close()` in dispose()
B
Call `cancel()`
C
Set it to null
D
Nothing, it's automatic

Frequently Asked Questions

What is the difference between a `Future` and a `Stream`?

A Future represents a single asynchronous value that will be available at some point. A Stream represents a sequence of values delivered over time. Use Future for one‑time operations (e.g., an API call), and Stream for continuous data (e.g., user input, real‑time updates).

When should I use `StreamBuilder` instead of manually listening?

StreamBuilder is the preferred way to display stream data in the UI because it automatically handles subscription and rebuilding when new data arrives. It also manages the subscription lifecycle, ensuring you don't have to manually cancel it. Only listen manually if you need to perform side effects (e.g., logging) without affecting the UI.

How do I handle errors in a stream?

When listening, provide an onError callback: stream.listen(onData, onError: (e) => ...). With StreamBuilder, the snapshot has an error property: if (snapshot.hasError) .... You can also use handleError on the stream before subscribing.

Can I reuse a stream after it's been closed?

No, once you close a stream (by calling close on the controller), no more events can be added. You need to create a new stream if you need more data.

Previous

flutter future

Next

flutter animations

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.