Stream in Flutter

Topics Covered

Overview

Streams in Flutter are a powerful mechanism for handling asynchronous events and data flow. They allow for real-time updates and reactive programming, enabling developers to build responsive and interactive applications. With streams, you can create a sequence of events or values that can be listened to and processed as they occur. By using StreamControllers, you can emit values onto a stream and manage subscriptions. Streams are commonly used for tasks such as real-time data updates, user input handling, network requests, and state management. They promote efficient resource usage and separation of concerns in Flutter development.

Introduction

Streams are a fundamental concept in Flutter for handling asynchronous events and data flow. They provide a powerful mechanism for managing and processing sequences of events or values reactively and efficiently.

Streams are analogous to pipes, where values flow from one end and listeners at the other end pick them up. With streams, you can easily implement reactive programming patterns and build applications that react to user actions and data updates in real time.

A stream can emit different types of events or values, such as integers, strings, objects, or custom data structures. These events can occur over time, allowing applications to handle continuous data updates and real-time interactions.

What is a Stream?

At its core, a stream is a sequence of asynchronous events or values. It represents a source of data that can be listened to and processed as the events or values are emitted. Streams follow a unidirectional flow, where events or values are emitted from the source and consumed by listeners or subscribers.

Events or values are emitted from a source and consumed by one or more listeners or subscribers. The stream source is responsible for producing the events, and the listeners register themselves to receive and process those events. This unidirectional flow allows for decoupling the producers and consumers, promoting separation of concerns and enabling efficient data flow management.

It represents a source of data that can be listened to by multiple subscribers. Streams can emit various types of events, such as user input, network responses, or changes in data. These events are delivered asynchronously, which means they can arrive at any time and in any order. Stream-based programming allows you to handle these events and update your application's UI accordingly.

How to Create a Stream in Flutter?

In Flutter, you can create a stream in two primary ways: using a StreamController or directly using the Stream constructor. Both approaches allow you to define a source of events or values that can be listened to and processed by subscribers.

1. Using StreamController:

The StreamController class acts as a source of data for the stream. It provides methods to emit events or values onto the stream and manage subscriptions. Here's how you can create a stream using a StreamController:

In the above code snippet, we create a StreamController<int> instance named myStreamController that will emit integer values. We add values to the stream using the add() method. Finally, we close the stream using the close() method to indicate that no more events will be emitted.

2. Using Stream Constructor:

The Stream class in Flutter provides a constructor that allows you to create a stream directly from a predefined sequence of events or values. This is useful when you have a fixed set of events to emit. Here's an example:

Note: It's important to note that when using the Stream constructor, the stream is static and does not support adding new events dynamically. If you need to emit events at different times or based on certain conditions, a StreamController is a more suitable option.

How to Use a Stream in Flutter?

Using a stream involves two primary components: the stream source (such as a StreamController or a Stream) and one or more stream listeners or subscribers.

1. StreamBuilder:

The StreamBuilder widget is a convenient way to listen to a stream and update the user interface based on the emitted events. It takes a stream as input and provides a builder function that gets called whenever a new event is emitted. Here's an example:

In the code snippet above, myStream is the stream we want to listen to. The builder function takes two arguments: the current build context and an AsyncSnapshot that represents the state of the stream. Inside the builder function, we can check if the snapshot has data using snapshot.hasData. If data is available, we extract the value using snapshot.data and update the UI accordingly. If the stream is in another state, such as waiting for data or encountering an error, we can handle those cases as well.

2. Manual Subscription:

If you need more control over the subscription process or want to perform custom operations when handling stream events, you can manually subscribe to a stream using the listen() method of StreamSubscription. Here's an example:

In the code snippet above, myStream is the stream we want to listen to. We use the listen() method to subscribe to the stream and provide callback functions to handle different scenarios. The first callback handles the emitted value, the onError callback handles any errors that occur during the stream processing, and the onDone callback is called when the stream completes. Finally, we can cancel the subscription when it is no longer needed using the cancel() method.

Emit/Add a Value onto the Stream

Emitting or adding a value to a stream involves using a StreamController.

The steps to follow are:

  • Create a StreamController

  • Emit a Value onto the Stream: You can emit or add values onto the stream using the add() method. This method allows you to manually push data onto the stream. Here's an example:

  • Handle Error Conditions:
    When emitting values onto a stream, it's important to handle potential error conditions. You can do this by wrapping the add() method call with a try-catch block. This allows you to gracefully handle any exceptions that might occur during the emission process. Here's an example:

  • Close the Stream:
    After you have emitted all the necessary values onto the stream, it's good practice to close the stream using the close() method of the StreamController.

Managing the Stream in Flutter

Managing a stream in Flutter involves controlling its lifecycle, handling subscriptions, and performing cleanup tasks. Here are the key points to consider when managing a stream:

1. Subscription Management:

  • Use the listen() method of StreamSubscription to subscribe to a stream and receive emitted events or values.
  • Store the subscription object returned by listen() to manage the subscription, including canceling it when no longer needed.
  • Cancel the subscription using the cancel() method to stop listening to the stream and release associated resources.

2. Stream Completion:

  • Streams can complete, indicating that no more values will be emitted.
  • Use the onDone callback in listen() or the StreamSubscription to handle stream completion and perform cleanup tasks.

3. Error Handling:

  • Handle errors that occur during stream processing using the onError callback in listen() or the StreamSubscription.
  • Implement error handling logic within the callback to gracefully handle errors and prevent application crashes.

4. Resource Cleanup:

  • Properly manage resources associated with the stream, such as closing network connections, releasing file handles, or disposing of other system resources.
  • Use the onDone callback to trigger cleanup operations and release resources when the stream completes.
  • Cancel subscriptions using the cancel() method when they are no longer needed to free up resources.

5. Memory Management:

  • Be mindful of memory consumption when working with long-lived streams or streams that emit large amounts of data.
  • Avoid memory leaks by canceling subscriptions and closing streams appropriately.
  • Consider using stream transformers or operators like where, take, or debounce to filter or limit the data being processed and reduce memory usage.
  • Stream Pausing and Resuming:
    • Use stream controllers with the pause() and resume() methods to pause and resume the emission of events on the stream.
    • Pausing a stream allows you to temporarily suspend the delivery of events while resuming restarts the emission.

6. Multiple Subscribers:

  • Streams in Flutter support multiple subscribers, meaning multiple listeners can listen to the same stream simultaneously.
  • Each subscriber receives the emitted events independently and processes them based on their specific logic.

7. Error Handling and Data Transformation:

  • Utilize stream transformers and operators like map, where, transform, or custom transformers to perform data transformation, filtering, and error-handling operations on the stream.
  • These operations allow you to manipulate the stream's data before it reaches the subscribers.

Cancel a Stream

Canceling a stream involves terminating the subscription and stopping the listening process for the stream. Canceling a stream is essential to release resources, prevent memory leaks, and ensure efficient memory management in your application. Here's how you can cancel a stream:

  1. When you subscribe to a stream using the listen() method, it returns a StreamSubscription object. To cancel the subscription and stop listening to the stream, you can call the cancel() method on the subscription object.

  2. If you are using a StreamController to emit events onto the stream, you can also cancel the stream by closing the StreamController using the .close() method. Closing the StreamController signals that the stream has been completed, and no more events will be emitted.

Common Stream Errors in Flutter

A common Stream error that is often encountered is “Stream already subscribed to” message or "Bad State: Stream has already been listened to".

The error occurs when you try to listen to a stream that has already been listened to or canceled. To avoid this error, make sure you cancel the existing subscription before listening to the stream again.

The reason for that is that there are two types of Streams, Single Subscription Stream and Broadcast Stream.

You can use both for individual events, but just be wary of the subscription policy on the first one. When using a StreamBuilder in Flutter you'll most likely always get the exception because the Stream will be subscribed to multiple times during build function calls (which happens a lot).

Fixing the Bad State Stream Error in Flutter

To fix the "Bad State: Stream has already been listened to" error, you can simply use the .broadcast() method. This sets the stream controller to a broadcast stream, enabling multiple listeners.

Manual Streams

An alternative and commonly used method to create streams in Flutter is through an async* function. This function runs asynchronously and yields a value whenever there's a new one, without halting the execution of the function. To illustrate this, consider the following code snippet:

In this code, getRandomValue() is a Future that returns a random value after a 1-second delay. To obtain multiple random values, you need to call and await the function multiple times.

However, what if you want to continuously receive random values from the function without stopping its execution? This is where async* and yield come into play. Let's create a function that returns a stream and emits a new random value every second:

This is known as a generator function. Notice the differences compared to the previous code snippet:

  • The function now returns a Stream instead of a Future, indicating that we need to subscribe to the stream instead of awaiting a single value.
  • The use of async* instead of async. This indicates that the code should run asynchronously but continue execution after yielding a value.
  • Instead of return, we use yield. This acts as a return statement but does not exit the function. It allows the function to continue executing the remaining code after yielding a value.

To use this stream (generator function), you can subscribe to it as follows:

Each emitted value will be printed with a 1-second delay between them. The streams created in this manner are broadcast by default, allowing for multiple subscriptions.

Advantages of Using Streams in Flutter

Streams provide several advantages when developing Flutter applications. Here are some key benefits of using streams:

1. Asynchronous Data Flow:
Streams enable asynchronous data flow, allowing your application to handle events and data updates as they occur. 2. Real-time Updates:
With streams, you can easily implement real-time updates in your application, ensuring that the UI reflects the latest data. 3. Reactive Programming:
Streams support reactive programming patterns, making it easier to build responsive and interactive user interfaces. 4. Efficient Resource Usage:
Streams provide an efficient way to handle asynchronous operations by using resources only when needed. 5. Separation of Concerns:
Streams help separate the data layer from the UI layer, promoting clean architecture and modularity in your Flutter application.

6. Stream Controllers in Flutter:
Stream controllers are a vital component of working with streams in Flutter. They act as the source of data and provide methods to add values to the stream. Stream controllers also allow for fine-grained control over the stream, such as canceling the stream or controlling its behavior as discussed and demonstrated above.

Example Application

Here, We are building a very simple Flutter app that demonstrates the use of a stream to display random numbers on the screen continuously.

The code and working of the app is as follows:

main.dart file:

The output of the above-built application will look something like this:

example-application-of-using-flutter-app

Use Cases of Streams in Flutter

  • Real-time Data Updates:
    Streams are ideal for handling real-time updates, such as displaying live chat messages or stock market prices.
  • User Input Handling:
    Streams can be used to handle user input events, such as button clicks or text field changes, and update the UI accordingly.
  • Network Requests:
    Streams are commonly used when making network requests. They allow for handling responses as they arrive, showing loading indicators, and updating the UI with the received data.
  • State Management:
    Streams are a core part of state management solutions like BloC (Business Logic Component) or Redux. They enable data flow and communication between different components in the application.
  • Animation and Gestures:
    Streams can be used to handle animation frames and gesture events, enabling smooth animations and interactive user interfaces.

Conclusion

  • Streams in Flutter provide a way to handle asynchronous data flow, allowing you to process and react to events or values as they occur.
  • Streams are like pipes where you can put a value on one end and have a listener on the other end to pick it up.
  • You can create a stream using StreamControllers, which act as the source of events or values for the stream.
  • StreamControllers allow you to emit values onto the stream using the add() method and handle subscriptions with the listen() method.
  • You can also create streams using generator functions with the async* and yield keywords, allowing for continuous emission of values without blocking execution.
  • Streams can be managed by handling subscriptions properly, closing StreamControllers when no longer needed, and using broadcast streams for multiple simultaneous listeners.
  • Stream errors, such as the "Bad State" error, can be fixed by using broadcast streams or applying appropriate error handling techniques.
  • Streams offer several advantages, including asynchronous and event-based data processing, improved memory efficiency by processing data as it arrives, and the ability to handle infinite data streams.
  • StreamControllers provide additional control over streams by allowing you to pause, resume, and cancel the stream as needed.
  • Streams have various use cases in Flutter, such as handling user input, processing real-time data from APIs or databases, implementing reactive UI updates, and managing state in complex applications.