Speed up Dart/Flutter with Futures, Streams and StreamTransformers
By: M De Kuijper
Tags: Dart, Mobile App Development, Flutter, Asynchronous, Optimization

Our current system works fine, but we can do better. Our major bottleneck right now is that we end up waiting a lot. First, we wait for the asset to be downloaded. Once it’s downloaded we wait for the asset to be saved. This may be okay if it was one asset, but with many assets, we might end up waiting for minutes. What we really need is a system that downloads assets in parallel.

The proposed situation

Futures

If you have used Dart/Flutter for some time, you should already be familiar with a Future . A very basic explanation of a Future is code that gets executed and returns a response sometime in the future. We don’t have to wait for the response, but we can wait for the response at any time we want.

This sounds like the perfect solution. We’ll create our Future to first download our file, and then save the file. For this example, we’ll use a delayed future with a DownloadAsset object to simulate this behavior:

And to download we’ll write the following code:

Running this code will result in the following (twice, because of the one-liner):

Downloaded and saved Small Asset

Downloaded and saved Medium Asset

Downloaded and saved Big Asset

Great, it seems to work. The small asset completes first, the medium asset second, and the big one last. Just like we expect in a real scenario. There is just one issue with this solution: We do not control when each future gets executed. We don’t want to try to download an asset when we don’t have an internet connection for example. So this solution is not ideal for our situation.

But, we can do better.

Streams

Streams are less common than futures, but they are just as useful. For those new to streams, it is a technique to handle the flow of data from input to output. They are similar to futures in the way that they are both asynchronous. This means we don’t have to wait for them while they do their operations. The difference is that in general: Streams execute operations many times; Futures execute operations once.

An advantage of streams is that even with many operations we can control the flow of data. This is very useful for our use case. We like to pause downloading when there is no internet connection and continue when we get connected again. We could even stop downloading all together, but we don’t need to for our project. There are other advantages such as memory management, but we won’t get into that, since it is not relevant for our situation.

StreamController

Streams can be created in many different ways. For our use case, we’ll use a StreamController<T> . The StreamController<T> allows us to add new items to the stream at any time we like. This way we can add our failed downloads back into the stream to try again.

Here is a quick overview of the StreamController<T> :

Stream manipulation

Once we have created our Stream<T> , we can manipulate and process the data flowing through it.

The stream has many functions to manipulate and process data for example: map() , asyncMap() , skip() , skipWhile() , take() , takeWhile() , expand() , asyncExpand() , transform() and many more. There are a few of them we will use, and explain briefly.

The map() function can be used to manipulate data. The output will always have the same number of items as the input, but they don’t have to be of the same type. Here are some examples:

asyncMap() is almost the same as the map function. The only difference is that it will wait for Futures to complete and returns a Stream<T> instead of a Stream<Future<T>> .

With the skipWhile() function we can place restrictions on the data we want to see and reduces the amount of data flowing through.

Stream subscription

StreamSubscription<T> can be created by calling listen() on any stream. In general, a stream can only have one subscription. There is however one exception, which is the broadcast stream. Only a broadcast stream can have multiple subscriptions.

In the listen function we can handle certain events at the end of the stream. In our stream, only the assets that failed to download will pass through. We want to add these assets back into the stream to try downloading again like this:

Now say our internet connection drops in the middle of our download. The first thing that will happen is that our downloads will start failing. This is not a problem, because we’ll just add them back. But there is no use to keep trying to download until we get connected again. So we can simply pause our stream:

Our stream can also be stopped flowing, by canceling it. Keep in mind that a broadcast stream needs to be canceled by all of the subscriptions before it stops flowing.

Lastly, we can also wait for the stream to complete with the following line of code:

Again, keep in mind that the StreamController should be closed once you are done adding items to it. The stream will never complete if you don’t close it.

Putting Everything together

Our code has become a bit more complicated, but if you followed every step along the way you should be able to understand everything.

Running this code will result in the following:

Downloaded and saved Big Asset

Downloaded and saved Small Asset

Downloaded and saved Medium Asset

So this shows a clear downside of using streams. All data in the streams will be processed one by one. So we are waiting again for our larger asset to complete, before the next one gets downloaded. It seems like we are back to where we started. However, we have setup our foundation for the perfect solution.

Because, we can do better.

Unordered Asynchronous Streams / Parallel Streams

What if I told you we can have both the benefits of streams and futures. This is unfortunately not a native feature in dart. We need to implement this functionality ourselves with the StreamTransformer<S,T> .

The StreamTransformer<S,T> allows us to take in as much data as we need, and transform it any way we like. This means we can take in a stream of futures and return back the results in any order. Which is exactly what we need.

Step 1: Binding the stream

For our own, StreamTranformer<S,T> we will need to create a new class that overrides the BaseStreamTransformer<S,T> . The base class requires the bind function to be implemented. The bind function takes in the old input stream and returns a new output stream.

When a stream gets bound to our transformer, there are a few things that need to be done. We need to create a subscription to our old stream, we need to make sure the cancel() , pause() and resume() flow controls are handled, we need to let our transformer know when our input stream has no more data left, and we also need our own internal _streamController. With all this our stream transformer setup will look like this:

The _onCancel() function cancels our subscription from the old stream and sets our _finalizing flag like this:

Step 2: Creating our constructor

We want our transformer to call a function to execute a new asynchronous task once data comes in. It would also be nice to have a feature that allows us to decide how many futures can be run at the same time. So in essence, we want our transformer to be called like this:

To achieve this we will take in a function as a parameter and an optional parameter called parallel. The function could return a Future or just the value. Dart has a FutureOr<T> type that enables both cases. We also need to initialize our _finalizing , _futureCount and our _streamController fields. With all this in mind our constructor looks as follows:

Step 3: Handeling incomming data

Our listener callback _onData will handle all our incoming data. We will call our _asyncCB() function. If the function returns a value back immediately we’ll add it to the _streamController . When _asyncCB() returns a future, we’ll increase our _futureCount and provide a callback to said future. We’ll talk more about this callback in the next step.

Once the _futureCount has reached the maximum amount of parallel futures, we need to pause our incoming stream. To pause the incoming stream we will first create a Completer. A completer allows us to create a future and complete them at a later time. Then we set our _resumeCallback to the completers complete() function and call the pause() function from our subscription with the completer’s future.

Step 4: Handling future completion

When a future completes, there are a few things we want to do. First, we want to decrease our _futureCount and add the value to the _streamController . When our _futureCount is below the maximum, we also want to resume our stream again if it is paused. And finally, once the last future completes, we want to close the stream.

Our final solution

Now that we have created our StreamTransformer class, we only have to change the following line

into

All our code can be found in this GitHub gist. Running the code will result in the following:

Downloaded and saved Small Asset

Downloaded and saved Medium Asset

Downloaded and saved Big Asset

Great! It works the way we had hoped for. Just like with futures the small asset completes first, the medium asset second, and the big asset last. However, now we have more control and can pause our operations at any time we like.