Futures: Promises in Dart

Futures: Promises in Dart

Asynchronous programming in Dart using Futures

BG Image — Unsplash

Dart is a language that was brought to life by Google when they decided to throw it at Flutter.

Dart is quickly becoming one of those languages beginners prefer to learn because it supports development for the major three platforms: iOS, Android, and Web. It has a lot of catching up to do with JavaScript, but common JS has been around for more than two decades — it’s unfair to compare it with a language that’s still maturing.

That being said, it has almost all the essential features of modern languages already, and in this article, we’re going to talk about one such very essential programming pattern. Asynchronous programming (or parallel programming) is a programming technique in which a particular task that can’t be executed instantly continues its execution in the background without blocking the whole program. A typical everyday example would be HTTP API calls.

Now if you’re coming from a JS background, you’ll be familiar with async-await, promises, or even subscriptions. In this article, we’ll talk about asynchronous programming in Dart.

Futures in Dart

Futures is one of the simplest and most basic APIs that Dart has for async.

Future is pretty much just like promises but in Dart. And just like any basic promise, it has three states: the execution state (completion state which can be separated as “completed successfully” and “completed with an error”).

Dart futures also run on the event loop, which manages the event queue. As Dart is a single-threaded program just like JS, all the tasks you write end up in the event loop for execution.

Think of an event loop as a ferry wheel — but where tasks are people.

When new people come in for the ride, they’re added to a cabin, and they stay in the wheel until their time is done. Similarly, tasks come in and stay in the event loop until its execution completes. But just like how the wheel doesn’t stop to wait until one user completes the ride, an event loop doesn’t stop execution for one particular task — even though each task may take a different amount of time to complete execution.

Let’s consider an example to make things clear. The code below illustrates a very simple scenario where you have a download button. Clicking on it will cause an image to be downloaded and shown in the app. Let’s break it down.

Line 1–2: As soon as the user taps the RaisedButton, a tap event is registered in the event loop that calls the tap handler. Now the execution state has started, which will be uncompleted.

Line 3: The tap handler uses the HTTP library to make a get request for the image from the URL. This returns a future into myFuture.

Line 4: We register a callback using then to do something when the request is completed.

Now the event loop will not wait until the request is completed, but in the meantime, it’ll continue executing other tasks from the event queue without waiting for the execution of the HTTP request to be completed.

Line 5: Eventually, once the HTTP request successfully returns the data to the future, this data gets passed to the callback in the response object, and the callback is triggered to show the image in the app.

If you are new to this whole promise thing, one thing you may not have noticed here is we never had to manage the event loop directly which wouldn't have been the case if not for the future API.

Future took care of all the handling in the event loop, and all we had to do was create a future object from the HTTP request and tell it what needs to be done once the future is completed.

But just like I mentioned above, a future can also have a third state which is completed without an error, and while writing efficient code you have to cover the error to ensure that your program doesn't break.

Line 6: If the future completed with an error, the execution comes to this line where we have defined the callback for the error using the catchError() method. This method will take in the error object instead of a value and execute the statements inside whenever the future completes with an error.

Creating an Instance of Future

In the above example, we didn't exactly create a future on our own. If you have noticed, the HTTP library did this job for us — all we had to do was make a call to the HTTP get method and assign this to the created future object myFuture.

final myFuture = http.get(‘http://example.com');

Similarly, there are many other libraries that generate a future for you. Imagine you are storing some data into the shared preferences of the app. Getting an instance of the shared preferences also returns a future.

final myFuture = SharedPreferences.getInstance();

All that is good, but how do we create a future without libraries? That’s what the future constructor is used for.

Future constructors are constructors that takes a function and return a future that matches the function’s return type. The function runs asynchronously, and once the function returns a value, the future gets completed with that value. Let’s consider an example:

Creating a future using the constructor

Just like our last example, we’re creating a future but this time using the Future() constructor. The future constructor will return an uncompleted future at first execution. But the event loops will not wait until the future is completed and will execute the print statement on line 6 immediately. So the output of the above code will be:

Printing....
Future Complete

Future.value() is another constructor you can use when you already know the value that needs to go in. This is very useful in cases when you have already got the value that you need, like when you’re building services that use caching. But one thing to remember here is that the future still completes asynchronously. And this is how you assign it:

final myFuture = Future.value(10);

There is another constructor that does the exact opposite of Future.value() and completes with an error. The [Future.error()](https://api.dartlang.org/stable/dart-async/Future/Future.error.html) constructor takes an error object and an optional stack trace. Example:

final myFuture = Future.error(Exception());

And to finish off we have the Future.delayed(), which works very similar to Future, but as the name suggests, it waits for a specified amount of time before executing the function and completing the future.

This can come in handy when you need to mock something which would be asynchronous and you have not completed writing the code yet.

The typical use case would be when you’re trying to mock an API call that may take a few seconds to complete, and you have written some code that’ll let the user know that something is happening in the background, like a loader animation. The simplest way to create one is:

Future.delayed() example.

Conclusion

That’s it — we’ve covered the basics of Futures in Dart.

Now diving deeper into its uses and implementation in real-time applications, like Flutter apps, is something that we need to look into more carefully, but we’ll keep that for another article since this was supposed to be a starter.

Did you find this article valuable?

Support I Speak Code by becoming a sponsor. Any amount is appreciated!