Dart – 24 – Streams and Stream Controllers

Streams and Stream Controllers in Dart

Streams and Stream Controllers are fundamental concepts in Dart for handling asynchronous data and events. They provide a structured way to work with sequences of data that are produced over time. In this discussion, we’ll explore the significance of Streams and Stream Controllers, understand how they work, and see practical examples of their use.

Understanding Streams

In Dart, a Stream represents a sequence of asynchronous events or data that can be listened to over time. Streams are used to handle data that may arrive in the future, such as user input, data from a network request, or events in a user interface. Streams provide a way to process data as it becomes available.

Creating and Using Streams

You can create a Stream in Dart using a variety of sources, including built-in events like button clicks, custom event controllers, and data from asynchronous operations. Once a Stream is created, you can register listeners to react to events or data as they are added to the Stream.

Here’s an example of using a Stream to handle button click events:


import 'dart:html';

void main() {
  final button = ButtonElement()..text = 'Click Me';
  final output = ParagraphElement();

  document.body
      ..children.addAll([button, output])
      ..onMouseOver.listen((event) {
        output.text = 'Mouse Over Detected';
      });
}
    
Understanding Stream Controllers

Stream Controller acts as an interface for adding data or events to a Stream. It allows you to push data into a Stream and notify listeners when new data is available. Stream Controllers are essential for creating and managing custom Streams in Dart.

Creating and Using Stream Controllers

To create a Stream Controller in Dart, you instantiate the StreamController class. You can then access the Stream through the controller’s stream property and add data to the Stream using the add method. Listeners can subscribe to the Stream by using the controller’s stream.

Here’s an example of using a Stream Controller to create a custom Stream and add data to it:


import 'dart:async';

void main() {
  final controller = StreamController();

  final stream = controller.stream;
  stream.listen((data) {
    print('Received data: $data');
  });

  controller.add(1);
  controller.add(2);
  controller.add(3);
}
    
Asynchronous Data Handling

Streams and Stream Controllers are invaluable for handling data that arrives asynchronously. Whether you’re dealing with real-time data updates, user interactions, or data from external sources like databases or APIs, Streams provide a structured way to process the data as it becomes available.

Here’s an example of using Streams to handle real-time data updates:


import 'dart:async';

void main() {
  final controller = StreamController();

  final stream = controller.stream;
  stream.listen((data) {
    print('Received data: $data');
  });

  // Simulate real-time updates
  for (var i = 1; i <= 5; i++) {
    Future.delayed(Duration(seconds: i), () {
      controller.add(i);
    });
  }
}
    
Combining and Transforming Streams

Dart allows you to combine and transform Streams to create more complex data processing pipelines. You can use functions like map and where to transform the data within a Stream or use operators like asyncMap to perform asynchronous transformations.

Here’s an example of using the map function to transform data within a Stream:


import 'dart:async';

void main() {
  final controller = StreamController();

  final stream = controller.stream.map((data) => data * 2);
  stream.listen((data) {
    print('Received data: $data');
  });

  controller.add(1);
  controller.add(2);
  controller.add(3);
}
    
Handling Errors with Streams

When working with Streams, it’s crucial to handle errors gracefully. You can use the onError method to catch and handle errors within the Stream. This ensures that your application remains robust even in the face of unexpected issues.

Here’s an example of handling errors within a Stream:


import 'dart:async';

void main() {
  final controller = StreamController();

  final stream = controller.stream;
  stream.listen(
    (data) {
      print('Received data: $data');
    },
    onError: (error) {
      print('Error: $error');
    },
  );

  controller.add(1);
  controller.addError('Data Error');
  controller.add(2);
}
    
Conclusion

Streams and Stream Controllers in Dart provide a powerful and structured way to handle asynchronous data and events. Whether you’re dealing with real-time updates, user interactions, or data from external sources, Streams and Stream Controllers allow you to process data as it arrives, creating more responsive and efficient applications. Mastering these concepts is essential for building Dart applications that work seamlessly with asynchronous data.