Asynchronous programming has always been a hot topic in programming and will continue to be so. It’s the ability for a program to execute pieces of code without having to block the current execution path. You can either wait for the results or you can fire and forget. In this blog-post we will discuss asynchronous programming in Flutter and Dart!
Introduction to Asynchronous Programming
When I think about asynchronous code, such as an asynchronous function, I say to myself “Here is a function that does something and returns the results later“. Functions traditionally in programming languages such as C were executed, did their job, and returned to the caller as soon as they were done, blocking the caller from being able to execute multiple pieces of code at once. Those were synchronous functions, and they are available to-date in all programming languages out there.
Asynchronous functions are functions that allow the caller to continue with its work while the function performs some operations and at some point in the future lets the caller know that it’s done with its job. Let’s have a look at an example of a synchronous function:
// a function that calculates values from 0 // to 99 (inclusive) and returns the list // as its return value. This function can be created // in a more compact way using iterables but for the // sake of simplicity I've written it this way using // a traditional for-loop List<int> calculateNumbers() { final numbers = <int>[]; for (var i = 0; i < 100; i++) { numbers.add(i); } return numbers; } void testIt() { final values1 = calculateNumbers(); // now we have the values final values2 = calculateNumbers(); // we called the function a second time }
This code is not special at all. All it does is that it calls the calculateNumbers()
function twice and collects the results. What’s remarkable is that the testIt()
function has to wait for the calculateNumbers()
function twice, in a row, without being able to calculate the results simultaneously and continuing with its work. The values1
and values2
variables are in no way connected to each other either and they both can coexist without having to interact with each other.
These two functions are better suited to be written exactly the way they have been written here; ie. synchronous. The program enters their execution, executes the functions, collects any results available and returns the execution context to the caller (testIt()
function in this case).
These two functions are very plain and they take a relatively short amount of time to execute their tasks. Even if you run this code on a Raspberry Pi Pico, it will take a very short time to execute so you don’t necessarily have to be worried about the performance of execution.
Now imagine in Flutter that you are designing a screen that asks the user to choose the country they are from and the list of countries are fetched from a Firebase or a custom API endpoint. You have a UI that needs to be displayed to your users but the result of the API call is something that will not be immediately available to your user interface. In those cases, you will need to execute your API call asynchronous to your UI code; meaning that your UI will be displayed to the user with an empty list of countries, you then fire the API to fetch the list of countries, let the API return and then calculate the list of countries and refresh your UI. This is the basis of asynchronous code in Dart, Flutter and all other languages that support asynchronous programming such as Rust, Swift and C#.
Asynchronous programming in Dart and Flutter is based on two main classes, Future
and Stream
and we will look at those in detail in the coming sections of this document.
Basics of Asynchronous Programming in Flutter and Dart
One of the building blocks of asynchronous programming in Flutter and Dart is the Future
class. This class represents a piece of work that will be executed, as its name indicates, some time in the future, and possibly returns with some results later! Let’s have a look at creating a function that always returns the String
value of "Hello, World!"
but after a 1 second delay:
// a simple function that returns a string // after a delay of 1 second Future<String> getHelloWorld() => Future.delayed( const Duration(seconds: 1), () => 'Hello World', );
As you can see the result of this function is not a String
, but a Future<String>
. Future
class in Dart is a generic class. Read my article on generics in Dart to learn more about generics before continuing with this article, if you’re not comfortable with generics yet! This is how the Future
class is defined, at least its header so we can see the generic constraint:
@pragma("wasm:entry-point") abstract class Future<T> { /// A `Future<Null>` completed with `null`. /// /// Currently shared with `dart:internal`. /// If that future can be removed, then change this back to /// `_Future<Null>.zoneValue(null, _rootZone);` static final _Future<Null> _nullFuture = nullFuture as _Future<Null>; ...
As you an see the Future
class is a generic class that has a generic parameter named T
and when we returned Future<String>
from our function, we told Dart that we have a Future
class whose internal value is of type String
. As simple as that!
Now going back to the getHelloWorld()
function of ours, if we then try to print the return value of this function, you probably expect a String
, but look what we’ll get:
void testIt() { final str1 = getHelloWorld(); str1.log(); // Instance of 'Future<String>' final str2 = getHelloWorld(); str2.log(); // Instance of 'Future<String>' }
You probably were surprised to see that our function didn’t actually return an instance of String
, but instead we got an instance of Future<String>
. This is how it should be though. An instance of Future
encapsulates a work to be done in the future, as its name indicates, not when you call the function.
So how can we read the value of those Future
instances? Well, the answer is quite vague: it depends! If you want to simply read the value and consume it, then you need to use the await
keyword in Dart. Now the rule for using this keyword is simple: await
can only be used in a function that is marked as async
and our testIt()
function is not marked as async
right now so let’s remedy that:
void testIt() async { final str1 = await getHelloWorld(); str1.log(); // Hello World final str2 = await getHelloWorld(); str2.log(); // Hello World }
You can call this function itself from anywhere now. If you have a Flutter application, you can call this function upon every build phase of your main widget, as shown here:
class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { // here we are calling the testIt function // without waiting for it to finish since testIt(); return Scaffold( appBar: AppBar( title: const Text('Home Page'), ), ); } }
There are a few things that might be confusing to a new-comer here so let’s break them down. The build()
function gets called every time the HomePage has to be re-drawn on the screen. It then calls the testIt()
function without prefixing it with await
. So instead of writing await testIt()
we are simply calling the function with testIt()
. So how does this even work? This is one of the things that throws off a lot of new-comers to asynchronous programming.
What happened here is that although the testIt()
function is marked as async
, its result is void
, meaning that it essentially is returning a Future<void>
and basically its result is of no value to us. But then the question is: how can we not await the results but have the testIt()
function run asynchronous code? Well, the answer is simple. The testIt()
function is marked as async
, but can be called just like a normal function. Inside the testIt()
function, we are using await
to wait for response from the getHelloWorld()
function and that works fine simply because the testIt()
function is being called, and that’s sufficient for the asynchronous work to be done properly.
This however has been the cause of a lot of confusion in Flutter, so the Flutter team has created a linter rule to warn developers not to mark void
functions as async
. Go to your analysis_options.yml
file in your Flutter project and add the avoid_void_async
rule as shown here:
linter: rules: avoid_void_async: true
Once you’ve done this, if you go back to where we wrote the testIt()
function, you will see a warning as shown here:
// you will now see a warning here saying // "Avoid async functions that return void." void testIt() async { final str1 = await getHelloWorld(); str1.log(); // Hello World final str2 = await getHelloWorld(); str2.log(); // Hello World }
This linter warns us that any function that is marked as async
should have a return value of type Future
. Now this can be a Future<void>
or Future of any other data type, but it has to be a Future
. So let’s remedy that:
// fix the warning from our linter by marking // the return type of this function as\ // Future<void> instead of void Future<void> testIt() async { final str1 = await getHelloWorld(); str1.log(); // Hello World final str2 = await getHelloWorld(); str2.log(); // Hello World }
There are a lot of moving parts to basics of asynchronous programming with Flutter and Dart so let’s summarize some of the basics that we’ve learnt to this point:
Future
is an encapsulation of work to be done in the futureasync
is the keyword that is appended to a function which allows the function to use theawait
keyword- The
await
keyword allows anasync
function to call anotherfunction with the return value of type Future
and await for its result - If you call a function that returns an instance of
Future
, you don’t have toawait
its result. You can just call it - A function that returns a
Future
is not obliged to be also marked asasync
unless it wants to use theawait
keyword internally
Returning Future from a Non-async Function
A function that returns a Future
does not necessarily have to be suffixed with the async
keyword. The async
keyword is only added to a function declaration if the function wants to use the await
keyword, meaning that the function wants to wait for another async
function to do its job. Let’s see an example of a function that returns Future
without the async
suffix:
// this function does not use the "await" keyword // internally so it does not have to be marked as // an async function Future<Iterable<int>> get0to10() => Future.value(Iterable<int>.generate(10)); // this function however uses the "await" keyword // to wait for the result of the "get0to10" function // so it is marked as an async function Future<void> testIt() async { final numbers = await get0to10(); // logs (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) numbers.log(); }
The get0to10()
function returns a Future
instance but it is not marked as async
since it doesn’t have to use the await
keyword internally, meaning that the function itself is not waiting for any other asynchronous functions to complete their job before it can return its value. The testIt()
function however wants to wait for the result of the get0to10()
function so it is marked as async
! Furthermore, the testIt()
function has a return value of Future<void>
since it is marked as async
according to our avoid_void_async
linter rule which we set up earlier, and returns no values since functions that have the return value of void
(and by extension, Future<void>
) in Dart implicitly return void
if they have no return
statement built into their bodies.
Consuming a Future in Flutter
You have a function that returns a Future<T>
(T
being any data type) and you want to consume its value and perhaps display it to the user in Flutter. How exactly go about doing that? Well there are various ways of doing that depending on your use-case but let’s have a look at an example where you have a function that returns a Future
of list of names and you want to display those names to the user in a ListView
. Let’s build the function first:
// a function that returns an Iterable<String> to the // call-site, with a list of names, with a delay // of 2 seconds (hardcoded). We will then consume this // function in a FutureBuilder Future<Iterable<String>> getNames() => Future.delayed( const Duration(seconds: 2), () => [ 'Vandad', 'John', 'Jane', 'Doe', ], );
To consume and display the result of this future in Flutter, you will be needing a Widget! Everything that is displayed to the user in Flutter is a form of Widget and this is no exception. You want to consume the result of a function and display its result immediately to the user: for that you need to use the FutureBuilder
widget. As its name indicates, it consumes a future and builds a widget based on the result of the Future
. Keep in mind that a Future
can fail, hence you will need to take care of different states of the future, such as “done”, “waiting”, “active” and “none”. So let’s put this information to use and build our widget:
class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Display Names'), ), body: FutureBuilder<Iterable<String>>( // pass the function that returns a Future // to the future property future: getNames(), // you can optionally add an initial data // to your FutureBuilder and have the "builder" // function process this data while you are waiting // for the future to complete initialData: const [], // the builder function will be called every time // the future changes state, such as when it starts // loading, when it completes, or when it fails builder: (context, snapshot) { // the "snapshot" parameter is of type // AsyncSnapshot<Iterable<String>>. This data-type // has a property called "data" which is of the same // data-type as your Future which was passed to the // future property of the FutureBuilder switch (snapshot.connectionState) { // by checking the connection state, we can // determine what to show to the user // while the future is loading, or when it // fails, etc case ConnectionState.done: // when the future completes, we can access // the data that was returned from the future // by using the "data" property of the snapshot final names = snapshot.data ?? []; // note that AsyncSnapshot has two important // properties namely "hasData" and "hasError" // which you can use to your advantage to get // more information about the state of the // snapshot return ListView.builder( itemCount: names.length, itemBuilder: (context, index) { final name = names.elementAt(index); return ListTile( title: Text(name), ); }, ); default: return const Center( child: CircularProgressIndicator(), ); } }, ), ); } }
The most important thing to remember about this example is the use of FutureBuilder
class which can consume a Future
of any type and return a Widget to be displayed to the user inside its “builder” parameter. This widget is a consumer widget, meaning that it takes the value of a Future
and reads its contents, and turns that content into an actual Widget to be displayed on the screen.
But what happens if you have a button on your home page and upon pressing that button, you want to read the list of names coming back from getNames()
function and if it contains the name “Vandad”, then you want to display a dialog to the user? Well, that is very simple. In this particular example, we are not going to display the contents of the getNames()
function to the user. Rather, by pressing a button, we are going to consume the Iterable<String>
and search through it and depending on the result of our search, we will either display or not display a message to the user. Let’s change the HomePage
to take care of this example:
class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Display Names'), ), body: TextButton( // change the signature of the onPressed callback // to be an async function instead because inside // this function we are going to use the "await" // keyword onPressed: () async { // call the function that returns a Future // and then use the "await" keyword to wait // for the Future to complete final names = await getNames(); // then we can use the "names" variable // to do whatever we want with it, in this case, // we are going to search for the name "Vandad" // and if we find the name, we will show a dialog // to the user if (names.contains("Vandad")) { // display the dialog to the user with this code showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Hooray!'), content: const Text('Vandad is in the list'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ), ); } }, child: const Text( 'Check for "Vandad"', ), ), ); } }
This was another example of consuming a Future
in Flutter using a TextButton
. The TextButton
is not turning the result of the Future
into a Widget
; instead, it is consuming the result, processing it and depending on some logic (the if-statement), it then displays a dialog to the user. In the previous example, you saw an example of actually consuming a Future
and turning the result into a Widget
that the user sees on the screen using a FutureBuilder
.
Consuming a Future in Dart
In the previous section we learnt to consume the result of a Future
in Flutter and display it to the user. Since widgets are a Flutter concept, in Dart, we need to resort to using a print statement of sorts to relay information to our users in the command-prompt, or perhaps if you are using Dart on the server, you might want to store the information in a database for instance. So let’s bring back our getNames()
function with updated documentation to make it applicable for a Dart app:
// a function that returns an Iterable<String> to the // call-site, with a list of names, with a delay // of 2 seconds (hardcoded). We will then consume this // function in the main function of our Dart app! Future<Iterable<String>> getNames() => Future.delayed( const Duration(seconds: 2), () => [ 'Vandad', 'John', 'Jane', 'Doe', ], );
How do we consume this in our main()
function now? Well, the answer is simple and it’s something we’ve already done before! We need to turn our main()
function into an async
function so that inside it we can use the await
keyword to wait on the result of the getNames()
function, as shown here:
Future<void> main() async { // wait for the result of the "getNames()" // function and then check for the existence // of the name "Vandad" in the list of names final names = await getNames(); if (names.contains("Vandad")) { print("Hello Vandad"); } else { print("Hello Stranger"); } }
This example was very similar to the TextButton
example of the Flutter consumption of a Future
which we looked at in the previous section of this article. So you have a Future and you want to consume its value; you can simply do so using the await
keyword and in order to be able to use the await
keyword inside a function, that function has to be marked as async
. Also take note of the return value of the main()
function as well which we have changed from void
to Future<void>
as dictated by the avoid_void_async
linter rule which we have set up before!
Future Error Handling in Flutter
Up to this point in the article we have been creating functions that return Future
instances plus we have consumed those instances using async
functions in Dart and Flutter, plus an example of FutureBuilder
in Flutter. But we have not even had a look at how errors are thrown in instances of Future and how we can handle those errors and how those errors affected the flow of our application.
In this section we will look at handling errors inside Future
instances in Flutter. Let’s have a look at an example where we validate a person’s age to be a teenager or not. If the age falls between 13 and 19 inclusive, then the person is a teenager, otherwise, not. What we will do to make this example more spicy is to add a function that simulates making an API call which retrieves the valid ages of a teenager:
// a function that simulates an API call which allows // us to retrieve the Iterable<int> representing the // valid ages of a teenager. This function returns an // iterable that contains the following values: // (13, 14, 15, 16, 17, 18, 19) after a 2 seconds delay Future<Iterable<int>> getTeenagerAges() => Future.delayed( const Duration(seconds: 2), () => Iterable<int>.generate(7, (index) => index + 13), );
This function’s return value is a Future
that contains an Iterable<int>
which itself contains the valid ages of a teenager. So if you search within this iterable for 13 or 14, you will find the values, but if you search for 20, you won’t, since a person of 20 years old is not a teenager.
Now we can consume this API in our custom function that checks a person’s age and ensures that the person is a teenager. Otherwise, it throws an error:
// a function that simulates an API call which allows // us to retrieve the Iterable<int> representing the // valid ages of a child. This function returns nothing // and throws an exception if the given age is not // included in the valid ages of a teenager Future<void> makeSureIsTeenager(int age) async { // consume our API and get the valid ages of a teenager final teenageAges = await getTeenagerAges(); // if the given age is not in the valid ages of a teenager // then proceed to the next line if (!teenageAges.contains(age)) { // in Dart, you can throw any object // as an exception or error so here we // are throwing a custom String as an error throw 'You are not a teenager'; } }
Take note of the result of this function. This function returns no values (meaning it returns void
) and since it is an async
function, it should ideally return a Future<void>
instead of void
as we’ve talked about it before. But what it does internally is that it stays silent if the given age is a valid teenager age; if not, it throws an exception with a custom String
that we have provided to it.
Now that we have this Future
, we can consume it in Flutter using FutureBuilder
to validate a constant age of 20:
class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Display Names'), ), // the "makeSureIsTeenager" function is called // by the FutureBuilder and since the result of this // function is void, we mark the type of the builder // as void as well body: FutureBuilder<void>( // call our function with the constant age of 20 here // and wait for the result to come back future: makeSureIsTeenager(20), // the builder parameter function will be called // upon changes to the future's state builder: (context, snapshot) { switch (snapshot.connectionState) { // once the future is "done", meaning that it // either finished with data or that it errored-out // the connection state will be "done" case ConnectionState.done: // if the snapshot contains an error, meaning that // our Future threw an exception, then we will // display the error message in a Text widget if (snapshot.hasError) { return const Text('This person is not a teenager'); } else { // otherwise, we will display a Text widget saying // that the given age is a valid age of a teenager return const Text('This person is a teenager'); } // in any other connection state, such as "waiting" // or "active", we will display a CircularProgressIndicator // to indicate that the future is still being processed default: return const Center( child: CircularProgressIndicator(), ); } }, ), ); } }
Take particular note of the hasError
property of our AsyncSnapshot
value snapshot
. This property will be true
if the Future
we fed our FutureBuilder
with threw an exception, in which case we will display a certain message to the user. Otherwise, when the snapshot state is still in done
, we can be certain that the Future
returned by our makeSureIsTeenager()
function did not throw an exception, meaning that the age passed to it indeed is a valid teenager age!
The way we have handled an error in this case is in the materialization of the Future
instance, meaning that we handled the error while we tried to display the result of the Future
as a Widget
instance. However, sometimes you will be consuming a Future
inside a button click for instance, and won’t necessarily display the contents of the Future
directly to the user, and in those cases you won’t have an AsyncSnapshot
instance to work with.
In the case of consuming a Future
directly without materializing it into a Widget
, you will need to work with try
and catch
blocks or alternatively absorb the errors. Let’s have a look at using try
and catch
first and we will continue using our makeSureIsTeenager()
function as before.
class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Age Checker'), ), body: TextButton( // we mark the onPressed function as async // because we are going to use the await keyword // inside of it onPressed: () async { // then we create a try-catch block // to catch any errors that might be thrown // by the "makeSureIsTeenager" function try { await makeSureIsTeenager(20); 'You are a teenager'.log(); } on String { // if the error is a String, then we // catch it and print it to the console 'String exception was thrown'.log(); } catch (error) { // otherwise we catch any other type of error // and print it to the console 'An error occurred'.log(); error.log(); } }, child: const Text('Make sure is teenager'), ), ); } }
Pay special attention to the onPressed()
function callback. In there we are using the try statement and in there we can await
on any async
functions that might throw an exception. Then we are using the on T
syntax to handle exceptions of a particular data type, in this case on String
will be called if the code inside the try
statement throws an exception of type String
. Finally we wrap the try
block with a catch-all statement of catch (error)
meaning that if the previous on T
statements don’t match the exception that is thrown, the catch (error)
statement will.
So now you know how to handle Future
exceptions in Flutter both while consuming the Future
as a Widget
and also consuming it by the means of awaiting on the result of the Future
.
Future Error Handling in Dart
Future
error handling in Dart is the same as consumption and error handling of Future
in Flutter where you use the try
–catch
block! There are no widgets in Dart so you don’t have to worry about FutureBuilder
or StreamBuilder
. Since we’ve already looked at an example for this in the previous section, I won’t go too much into it again but let’s for the sake of having a separate example for Dart, also write a pure Dart code without widgets to see how error handling works in Dart without Flutter:
Future<void> main() async { try { await makeSureIsTeenager(20); 'You are a teenager'.log(); } on String { 'String exception was thrown'.log(); } catch (error) { 'An error occurred'.log(); error.log(); } }
As you can see, there is nothing special happening in this code which we’ve not already seen in the previous section so I won’t go too much into details about the same code again.
Chaining Future Calls
In Dart, and by extension Flutter, if you have a Future
whose return value is needed to calculate the return value of another Future
, you can either await
on the first Future
and then feed its return value to the next Future
or alternatively you could chain the functions using the then()
function that is implemented on Future
.
Let’s say you have a an API that you pass an instance of int
to and it returns a boolean value telling you whether that integer representing a person’s age is for a teenager or not:
// this function will return a future that will complete // after a second and will validate the input as a valid // teenager's age, which is between 13 and 19 Future<bool> isTeenager(int age) => Future.delayed( const Duration(seconds: 1), () => age >= 13 && age <= 19, );
Now let’s also say that we have another API call that returns a JSON representing a user’s information, such as first name and age:
// a type-alias for JSON typedef Json = Map<String, dynamic>; // this is a function that will return a future that will // complete after a second and will return a json object // that contains the name and age of a person Future<Json> getJson() => Future.delayed( const Duration(seconds: 1), () => <String, dynamic>{ 'name': 'Vandad', 'age': 42, }, );
After we have these 2 basic functions, we can build a function that takes in a person’s name as its only parameter, extracts the JSON, looks for the name, and returns true
or false
if the name exists in the JSON and the age
of that person falls within a valid teenager’s age:
// a function that takes in a name and returns a boolean // that indicates whether the name is found in // the JSON returned by getJson() and that the age // is a valid teenager's age Future<bool> isTeenagerByName(String name) async { // get the json final json = await getJson(); // if the name is not found in the json, return false if (name != json['name']) { return false; } else { // if the name is found in the json, get // the age and validate it as a teenager's age final age = json['age'] as int; final teenagerValidation = await isTeenager(age); return teenagerValidation; } }
As you can see, the isTeenagerByName()
function is marked as async
because internally it uses the await
keyword twice, once for the getJson()
function and once for the isTeenager()
function. This function then ends up being quiet simple when you look at it, but it’s quite verbose. It would be great if we could cut this boilerplate code down a little bit and that’s exactly where the then()
function on Future
comes into play.
The then()
function gives you the value of the Future
after it is assumed to be awaited, and allows you to return a value inside the then()
or a Future
instance, effectively allowing you to chain Future
calls.
Let’s refactor our isTeenagerByName()
function and use the then()
function instead:
// this time we will use the "then()" method on // the future returned by getJson() to get the json // and at the same time validate the person's age // using the "isTeenager()" function right inside // the then() call Future<bool> isTeenagerByName(String name) => getJson().then( (json) { if (json['name'] == name) { // we don't have to await on the result // of the "isTeenager()" function because // the "then()" method can return a future // or a concrete value return isTeenager(json['age'] as int); } else { // in case we don't find the given name in the // json, we return a value of "false" to the caller return false; } }, );
Even though this looks like more code than the previous take, it is much less, there is just much more comments in the code to guide you than the previous example!
There are a few things that are interesting about this code. The first one being that this code is no longer marked as async
because it is not using the await
keyword internally. It’s also returning the result of isTeenager()
function directly inside the then()
function because the then()
function is able to return both a value or a Future
:
/// Note that futures don't delay reporting of errors until listeners are /// added. If the first `then` or `catchError` call happens /// after this future has completed with an error, /// then the error is reported as unhandled error. /// See the description on [Future]. Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});
Do you see the FutureOr<R>
type? That means the result of the then()
function can either be a Future<R>
or just R
as in a generic constraint for result. So by using the then()
function you can chain multiple Future
results without having to await
on each one at the cost of making your code perhaps slightly more complicated to the eyes of those not so used to Future
chaining using then()
. It’s a balance that only you and your teammates can strike.
Handling Errors in Chained Future Calls
When you use the await
keyword, you can bundle it inside a try
and catch
block but when you chain your Future
instances with the then()
function, you might want to handle your exceptions in a more functional way, since you are already inside a “functional” way of chaining Future
instances, you might as well handle your exceptions in a functional matter too.
Let’s make the coming example a bit more interesting. We are going to write a function that validates both the username and the password for a user before allowing the user to register and get a credential for logging in. So let’s start off by defining a list of invalid usernames:
// a list of usernames that our app has // reserved and has marked as invalid since they // are reservedf or system administrators const invalidUsernames = [ 'admin', 'root', 'administrator', ];
Then we go ahead and define a custom exception that we can throw inside our function later, to describe the situation in which we are asked to create a user with an invalid username:
// this is a custom exception that we will throw // when the user enters an invalid username to // register with class InvalidUsername implements Exception { final String username; const InvalidUsername(this.username); @override String toString() => 'Invalid username: $username'; }
Once our exception is in place, we can create a function that mimics the behavior of calling an API and validating a given username:
// this is a function that will check if the username // is valid or not. if it is not valid, it will throw // an InvalidUsername exception and if it is valid, // it will simply return Future<void> validateUsername(String username) async { await Future.delayed(const Duration(seconds: 1)); if (invalidUsernames.contains(username)) { throw InvalidUsername(username); } }
Then we will do the exact same thing for passwords. We will define a list of invalid passwords, a custom invalid-password exception and a function that validates a password by mimicking how an API would work:
// define a list of invalid passwords // because they are too common for instance const invalidPasswords = [ '123456', '123456789', 'qwerty', 'password', '111111', ]; // then just like we did with the username, we will // define a custom exception for the password // and throw it when the password is invalid class InvalidPassword implements Exception { final String password; const InvalidPassword(this.password); @override String toString() => 'Invalid password: $password'; } // this is a function that will check if the password // is valid or not. if it is not valid, it will throw // an InvalidPassword exception and if it is valid, // it will simply return without an exception Future<void> validatePassword(String password) async { await Future.delayed(const Duration(seconds: 1)); if (invalidPasswords.contains(password)) { throw InvalidPassword(password); } }
These pieces of code are pretty much identical to the “username” counterparts, only that they are concerned with the user’s password, and not the username. With these functions and classes in place, we can go ahead and write a function that returns a Future<bool>
that can validate a given username and password and if everything goes fine, then it can return a user credential. Let’s create a simple user credential class as well for the purpose of demonstration:
// an immutable user-credential class // that for now only has a username @immutable class UserCredential { final String username; const UserCredential({required this.username}); }
With all of the pieces in place, let’s go ahead and first write our registration function with plain async and await syntax and then turn it into a chained Future using the then()
function.
Future<UserCredential> register({ required String username, required String password, }) async { // validate the username and password await validateUsername(username); await validatePassword(password); // in here you can then go ahead and call an API // to actually register the user // for now we simply return a user-credential // if we have validated the username and password return UserCredential(username: username); }
Once we have this traditional looking register()
function, we can call it inside another function as shown here and get the credentials. Let’s start off by passing an invalid username to the register()
function:
Future<void> testIt() async { try { final credentials = await register( // pass an invalid username to the function username: 'admin', // the password is "valid" since it's not // one of the passwords in "invalidPasswords" password: 'oshfoiewhohoweh', ); credentials.log(); } on InvalidUsername { // upon receiving this exception, we can // show the user a message that the username // is invalid 'Username is invalid'.log(); } on InvalidPassword { // and the same thign for InvalidPassword exception 'Password is invalid'.log(); } catch (e) { // and upon any other exception, we can // show the user a generic error message 'Something went wrong: $e'.log(); } }
This code, though verbose, works. We call the register()
function and await the response. If that function returns a UserCredential
instance then we know both the username and password were validated and were deemed to be OK. This was the traditional way of writing the register()
function though so let’s use the then()
function to chain our two API calls:
// in this example we are chaning the futures // together using the .then() function instead // of using the await keyword Future<UserCredential> register({ required String username, required String password, }) => validateUsername(username) .then((_) => validatePassword(password)) .then((_) => UserCredential(username: username));
This code is just a teeny tiny bit more compact than its async
/ await
counterpart, and really achieves the same goal. The thing to notice about this code is how we are ignoring the input values to the then()
function using the (_)
syntax. Rust and Swift have the ability to ignore parameters but Dart doesn’t. The reason we are ignoring the input values is that neither the validateUsername()
nor the validatePassword()
functions actually return a value. They either are silent if things go well, meaning that their Future<void>
just returns a void
, or if things go wrong, meaning that validation fails, they throw an exception. So they basically have no return value, and that’s the reason we are using the (_)
syntax since the incoming parameter in the then()
function will be void
.
What would be exciting is if we changed the implementations of the validateUsername()
and validatePassword()
functions so that they return Future<bool>
instead of Future<void>
, with the value of true
and false
depending on whether the incoming values are valid or not. Let’s do that now:
// we have changed this function to return a Future<bool> // instead of a void + exception // it returns true if the username is valid // and false if it is invalid Future<bool> validateUsername(String username) async { await Future.delayed(const Duration(seconds: 1)); final isUsernameValid = !invalidUsernames.contains(username); return isUsernameValid; } // similar to the validateUsername function // we have changed this function to return a Future<bool> // instead of a void + exception // it returns true if the password is valid // and false if it is invalid Future<bool> validatePassword(String password) async { await Future.delayed(const Duration(seconds: 1)); final isPasswordValid = !invalidPasswords.contains(password); return isPasswordValid; }
The next step is to change the register()
function’s implementation so that it works with the new way the validateUsername()
and validatePassword()
functions work:
Future<UserCredential> register({ required String username, required String password, }) => // start off by validating the username validateUsername(username) .then((isUsernameValid) { // if the username is not valid // then throw an exception which will // prevent the next then block from executing if (!isUsernameValid) { throw InvalidUsername(username); } }) // if we get to this then() block that means // the username is valid, so proceed by // validating the password .then((_) => validatePassword(password)) // similar to the username validation, if the // password is not valid, then throw an exception // which will prevent the next then block from executing .then((isPasswordValid) { if (!isPasswordValid) { throw InvalidPassword(password); } }) // if we made it this far that means both the username // and password are valid, so we can proceed by // creating a new user credential object .then((_) => UserCredential(username: username));
The same functionality can easily be implemented using a simple async
await
block and some might argue that an async
await
equivalent of this code is actually more readable. And that’s up to every individual and team to decide what fits their needs best but for the sake of completeness, let’s have a look at how the same code can be written without then()
blocks:
Future<UserCredential> register({ required String username, required String password, }) async { // validate the username final isValidUsername = await validateUsername(username); if (!isValidUsername) { // if the username is invalid, throw an exception // and stop the execution of this function throw InvalidUsername(username); } // validate the password final isValidPassword = await validatePassword(password); if (!isValidPassword) { // if the password is invalid, throw an exception // and stop the execution of this function throw InvalidPassword(username); } // otherwise, return a new user-credential object return UserCredential(username: username); }
This code might look much more compact than the previous example but take note of much less comment as well in this last block of code. The point we need to reach at the end of this section is that you can achieve the same results using two different approaches, chaining Future
calls with then()
or simply using async
and await
and only you and/or your teammates can make the decision as to which method best fits your needs.
Up to this point we have seen how exceptions are thrown and handled in our Future
calls but we one thing we have not looked at is absorbing errors and perhaps acting on them before they reach the call-site. Let’s have a look at an example. Let’s say we have a signIn()
function that takes in a username and a password. If they match two hardcoded values we put in our app, then we sign the user in, otherwise, we actually register the user with a signUp()
function and then sign the user in. So let’s create the signUp()
function first:
// define the username that has already been // taken by someone else const alreadyRegisteredUsername = 'admin'; const alreadyRegisteredPassword = 'admin'; // and also a list of invalid passwords that // nobody should use for their account const invalidPasswords = [ '123456', '123456789', 'password', ]; Future<void> signUp({ required String username, required String password, }) async { // create a fake 2 seconds delay await Future.delayed(const Duration(seconds: 2)); // if the username is already taken, throw an exception // and let the caller handle it if (username == alreadyRegisteredUsername) { throw InvalidUsername(username); } // if the password is invalid, throw an exception // and let the caller handle it if (invalidPasswords.contains(password)) { throw InvalidPassword(password); } }
The only thing this function really does is that it compares the given username to the one we have defined in alreadyRegisteredUsername
constant and if they match, then it throws an InvalidUsername
exception meaning that we already have this user and a new user cannot register with the same username. It also compares the given password with list of invalid passwords and throws an InvalidPassword
exception if the password is found within the array.
We now want to create a signIn()
function that first checks if the username and password are already registered in the system, and if not, it will attempt to create the user by calling the signUp()
function internally:
Future<UserCredential?> signIn({ required String username, required String password, }) async { // create a fake 2 seconds delay await Future.delayed(const Duration(seconds: 2)); // if the username and password match the // already registered username and password // then return a user-credential object as in // signing the user in if (username == alreadyRegisteredUsername && password == alreadyRegisteredPassword) { return UserCredential(username: username); } else { // try to sign the user up try { // if the user is not registered, sign them up await signUp( username: username, password: password, ); // if the sign up was successful, return the user credential return UserCredential(username: username); } on InvalidUsername { // if the username was invalid, just return null return null; } on InvalidPassword { // if the password was invalid, just return null return null; } } }
What is noteworthy about this code is how we are handling errors. If the signUp()
function throws exceptions, we return null
from our function. In other words, we don’t propagate exceptions to the caller. So how can we rewrite the same function now without awaiting on signUp()
?
Future<UserCredential?> signIn({ required String username, required String password, }) async { await Future.delayed(const Duration(seconds: 2)); if (username == alreadyRegisteredUsername && password == alreadyRegisteredPassword) { return UserCredential(username: username); } else { // try to sign the user up return signUp(username: username, password: password) // if the sign up was successful, return the user credential // but take note of the return value being an optional UserCredential // since in onError we want to return null which is only compatible // with UserCredential? and not UserCredential .then<UserCredential?>((_) => UserCredential(username: username)) .onError((error, stackTrace) => null); } }
In this example we are using the onError()
function on Future
that allows us to catch exceptions that occur inside the Future
and allows us to continue with the Future
chaining by returning either a new Future
or a concrete value (such as an instance of String
or UserCredential
), or alternatively, throw a new exception that can be handled by the call-site or the next onError()
call.
One thing I need you to take note of is the then<UserCredential?>()
function call. This function call returns a UserCredential
for sure but it marks it as an optional credential, so that the onError()
function will be allowed to return null
, meaning that a null user credential or an optional one so to say. If you don’t mark the then()
function with that optional data type the onError()
function won’t be allowed to return null
as its return value.
Just remember that the onError()
function is allowed to either return a concrete value or a Future
, meaning endless possibilities:
Future<T> onError<E extends Object>( FutureOr<T> handleError(E error, StackTrace stackTrace), {bool test(E error)?}) { // There are various ways to optimize this to avoid the double is E/as E // type check, but for now we are not optimizing the error path. return this.catchError( (Object error, StackTrace stackTrace) => handleError(error as E, stackTrace), test: (Object error) => error is E && (test == null || test(error))); }
So now you have seen how you can handle errors when chaining Future
calls in Dart and Flutter; it’s now time we talk about streams and how they differ from futures.
Introduction to Streams in Flutter and Dart
Up to this point we have been looking quite deep into how Future
works and how it is the basis of asynchronous programming in Flutter and Dart. Future, however, can only complete with either an exception or a single value, or no values at all! But that’s not good enough for some scenarios!
Let’s say you are creating a chat application in Flutter and want to display chat messages in real-life. If the data in your chat screen was represented with a Future<Iterable<Message>>
, once the future completes, your chat screen will stay silent until the user refreshes it perhaps manually. What you want in that case is a stream of Iterable<Message>
, a sort of data that starts, and continues receiving new data until the user closes that screen. This data structure in Dart and Flutter is represented by the Stream
class.
Let’s also say that you want to display a countdown in your application where the screen shows a countdown starting from 10 all the way to 0 and then closes the screen automatically as soon as the value reaches 0. You can either create a timer as you would in JavaScript and do the countdown manually or you could use a Stream<int>
to represent the values. The stream will begin, then produce the value of 10, then after a second 9 and all the way down to 0 and then the stream will close, marking the end of it producing values.
When you are working with Stream
in Dart, you will be simply working with Stream
and perhaps StreamController
which is an object that allows you to easily represent a Stream
to the outside world but internally allows you to manipulate the data in the Stream
using a very simple API. We will look at these examples soon. But if you’re working with Stream
in Flutter, you are very likely to use the StreamBuilder
widget which is similar to the FutureBuilder
that we’ve already talked about, but represents the values inside the Stream
using an AsyncSnapshot
. We will look at all of these in the coming sections, so dont’ worry.
Stream
and Future
in Dart and Flutter are the pillars of asynchronous programming. We’ve seen how Future
works; so let’s dig into Stream
as well to complete the picture!
Consuming a Stream in Flutter
Instead of talking too much about abstract topics, let’s have a look at an example. Let’s say that we want to display a counter that starts from 0 and never ends, and present the current value of the counter inside a Text
widget in Flutter. How do we create such as Stream
though? Well, the Stream
class, just like the Future
class, has some handy functions that allow us to create different types of streams with ease. So let’s check out this example:
// create a stream that waits 1 second // between each emission and then // emits integers starting from 0 with // no explicit end Stream<int> counter() => Stream.periodic( const Duration(seconds: 1), (i) => i, ); class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( // we use StreamBuilder to consume our stream // and display the current value of the counter body: StreamBuilder<int>( // we use the counter() function to create // our stream stream: counter(), builder: (context, snapshot) { // we then look for the connection state of our // async snapshot and display the appropriate // widget based on the state switch (snapshot.connectionState) { // if the connection state is active, that means // the stream has been connected to and is ready // to emit values or is already emitting values case ConnectionState.active: final count = snapshot.requireData; return Text('Counter = $count'); // in any other connection state, we display // a simple Text to the user default: return const Text('Waiting...'); } }, ), ); } }
Now you’ve noticed that both StreamBuilder
and FutureBuilder
pass an AsyncSnapshot
to you which represents the various stages of the lifetime of a Stream
and a Future
respectively. For a Stream
, while the stream is active and is producing values, the connection state will be active
. For a Future
however, the connection state will be done
when it has produced its value and that does make sense if you think about it. Our counter()
function’s stream will never complete. It will terminate only if the call-site terminates its consumption with a modifier function, which we will talk about later but the way we are consuming this particular stream is that it starts with the value of 0 and it never ends!
This was an example of consuming a Stream
in Flutter in that we took the values inside the Stream
and we displayed them on the screen. But what if you want to consume the values of a Stream
without actually displaying them directly on a Widget
? Well, that’s similar to how we consume a Future
in Dart because pure Dart doesn’t have the concept of widgets. So let’s see how we can consume a Stream
in pure Dart now.
Consuming a Stream in Dart
Consuming a Stream in Dart is very similar to how we consume a Future
in Dart. We will be using the await
keyword. Let’s jump right into it. If we take the same Stream
as we had in the previous section:
// create a stream that waits 1 second // between each emission and then // emits integers starting from 0 with // no explicit end Stream<int> counter() => Stream.periodic( const Duration(seconds: 1), (i) => i, );
And then try to consume all the values in our main()
function:
import 'dart:developer' as devtools show log; extension Log on Object { void log() => devtools.log(toString()); } // create a stream that waits 1 second // between each emission and then // emits integers starting from 0 with // no explicit end Stream<int> counter() => Stream.periodic( const Duration(seconds: 1), (i) => i, ); Future<void> main() async { // then go through the values in our counter // and print them to the console await for (final value in counter()) { print(value); } }
You can see that we are using the new await for
syntax in our Dart example. This will work in Flutter as well, for instance in a button onPressed()
event! You can also turn the values inside a Stream
into a Future
and then into a List
. If you have a Stream<int>
, the data in this stream is a list of integers basically, and you can easily to that conversion by using the toList()
function of Stream
which turns the stream into a future and you can then await
on the future to get the values. However, one thing that is important to note is that our counter()
stream never finishes. It starts but it never finishes. At least the function that creates the counter does not have an end applied to the stream. However, a Stream
class has a function called take()
which takes in an integer as a parameter and cuts off the source stream after those many items. Here is an example:
Future<void> main() async { // take the first 5 emissions of the counter // and print them to the console await for (final value in counter().take(5)) { print(value); } }
Here even though the counter()
function does not designate an end to our stream, the call-site, the main()
function, can decide to consume maximum 5 elements from the stream and print them to the console. Note that if the stream ends before it has emitted 5 items, that’s fine too. We are talking about maximum number of elements, not minimum!
Creating Streams in Flutter and Dart
Up to this point we have only seen 1 example for how to actually create a Stream
instance and that was our counter()
example. But in Dart and Flutter, you can create a function that produces a Stream
using the async*
keyword. This is a keyword that we have not looked at yet so let’s dig into it.
Let’s say that you want to create a Stream
that produces 3 String values. You could either write the function like this:
// a simple stream that emits 4 names // and then closes Stream<String> produceNames() => Stream.fromIterable([ 'Vandad', 'John', 'Jane', 'Doe', ]);
You could alternatively suffix your stream-producing-function with the async*
keyword and then yield
your values inside the function as shown here:
// a function that produces 4 names in a stream // and then closes the stream. It is suffixed // with the keyword async* to indicate that // it is an asynchronous generator Stream<String> produceNames() async* { // using the yield keyword you can "insert" // values into the resulting stream yield 'Vandad'; yield 'John'; yield 'Jane'; yield 'Doe'; }
The yield
keyword allows you to insert a value into the resulting stream. The stream then closes implicitly when the function reaches its end. In other words, there is no keyword for closing the resulting stream when you’re inside an asynchronous generator function, which our produceNames()
function marked with async*
is!
Apart from the yield
keyword, you also have access to the yield*
keyword. The yield*
keyword allows you to insert another stream into your stream. Here is an example:
// a simple Stream with 3 values containing male names Stream<String> maleNames() => Stream.fromIterable([ 'John', 'Peter', 'Paul', ]); // similar to maleNames(), but produces female names Stream<String> femaleNames() => Stream.fromIterable([ 'Mary', 'Jane', 'Sarah', ]); // using the yield* keyword you can insert // other compatible streams into the resulting // stream of your function. You have to mark // your function as async* in order to first // turn the function into an asynchronous generator Stream<String> allNames() async* { yield* maleNames(); yield* femaleNames(); }
So asynchronous generators are the simplest way of creating streams in Flutter and Dart. But you can also use functions that are programmed on the Stream
class itself to create streams, such as Stream.fromIterable()
that turns an iterable into a stream with ease.
Creating Streams with Stream Controllers
Up to this point we have looked at creating streams using either the Stream
class directly such as Stream.fromIterable()
or using asynchronous generators with async*
and yield
/ yield*
keywords. You can also create a stream using a StreamController
. This class works similar to an asynchronous generator but gives you more power as to where it is being used. Let’s say that you have a HomePage
widget in Flutter and every time the user taps a button you want to insert the current date and time into a ListView
that you have inserted on the page. How can you achieve this with an asynchronous generator? The answer is: you can’t.
An asynchronous generator has a very specific job. To produce values that you have already programmed into it; a predetermined sequence, if you will. But when it comes to user-produced values that have to be placed inside a stream, you will need to use a StreamController
. Let’s see an example to best demonstrate how stream controllers work (this is quite a long example but the logic is very simple, so read the comments for more information please):
// we need a stateful widget because we need to construct // and then destruct our stream controller, so we are reliant // on initState() and deactivate() methods of State class class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { // we need to create a stream controller late StreamController<String> _controller; // collect all the values emitted by the controller // into a local variable final List<String> _values = []; @override void initState() { super.initState(); // construct your stream controller in initState() _controller = StreamController<String>(); } @override void deactivate() { // don't forget to close the stream once // your widget is being deactivated _controller.close(); // and also clear the local variable _values.clear(); super.deactivate(); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: StreamBuilder<String>( // feed the stream controller's stream to your // stream builder stream: _controller.stream, builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.active: // if we got a value, add it to our local variable if (snapshot.hasData) { _values.add(snapshot.requireData); } // and construct a ListView with all values // available in the local variable return ListView.builder( itemCount: _values.length, itemBuilder: (context, index) { final item = _values[index]; return ListTile( title: Text(item), ); }, ); default: return const Text('Waiting...'); } }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { // when the button is pressed, add the current // time to the stream controller's sink _controller.sink.add( DateTime.now().toIso8601String(), ); }, child: const Icon(Icons.add), ), ); } }
There are a few things to note about this code:
- After creating a stream controller by initializing it, you are responsible for closing it using its
close()
function. - Usually the best widget for instantiating and disposing of a stream controller is
StatefulWidget
because it gives you theinitState()
anddeactivate()
functions where you can instantiate and close your stream controller. - When adding values to a
StreamController
, use theadd()
function of the controller’ssink
property. There is also anadd()
function on the controller itself for convenience. - When consuming the stream of a stream controller, use its
stream
property.
A stream controller is both a read and write stream, meaning that you can both write to it and also read from it. So it’s a more convenient way of creating a stream in other words.
One important thing to understand about a stream, specially when looking at this example, is that a stream does not hold all its emitted values. A stream only emits values and forgets about them. In other words, a stream does not have a buffer that it holds its values inside for later use. It only emits values and it’s your job to keep hold of those values, like we are doing in our previous example with the _values
buffer variable.
Stream Error Handling in Flutter
Just like a Future
, a Stream
can also produce an error. If you have an async generator and want to produce an error or exception in the function, simply use the throw
keyword just like you would in any other place in Dart or Flutter. Let’s write a function that will produce a stream with a few String
instances and then throws an exception:
// an asynchronous generator function that throws // an exception which has to be caught by whoever // consumes the result of this function Stream<String> getNames() async* { yield 'Vandad'; await Future.delayed(const Duration(seconds: 1)); yield 'John'; await Future.delayed(const Duration(seconds: 1)); throw 'Enough names for you'; }
This function will produce 2 names before it errors out with an exception. This exception is automatically thrown into the resulting stream, so you shouldn’t yield
the error; you need to throw
the error in your asynchronous generator and Dart will automatically insert that error into your resulting stream.
Now if you want to consume this stream in Flutter, you will need to use a StreamBuilder
again and look at your AsyncSnapshot
‘s hasError
property to see if it contains an error. If yes, then you can extract that error using the error
property of the AsyncSnapshot
as shown here:
class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final _values = <String>[]; @override void deactivate() { _values.clear(); super.deactivate(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Stream error'), ), body: StreamBuilder<String>( stream: getNames(), builder: (context, snapshot) { switch (snapshot.connectionState) { // when a stream errors out, the snapshot will // go into the "done" state so if you are displaying // a different widget in the "done" state, you will // never be able to actually catch the error case ConnectionState.done: case ConnectionState.active: // check if we have an error and if yes, display // it to the user if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } // otherwise, if we have data, display the data if (snapshot.hasData) { _values.add(snapshot.requireData); } return ListView.builder( itemCount: _values.length, itemBuilder: (context, index) { final value = _values[index]; return ListTile( title: Text(value), ); }, ); default: return const Text('Waiting...'); } }, ), ); } }
You can see that this example is very simple to just consuming a stream without an error. Except in this case we are using the hasError
property of our AsyncSnapshot
and if it indicates that we have an error in the snapshot that represents our stream, then we can extract that error using the error
property of our snapshot.
You can also handle the errors of a stream using stream transformers, and that’s something we will talk about a little bit later!
Stream Error Handling in Dart
When talking about stream error handling in Flutter, we looked at AsyncSnapshot
and how we can use the hasError
and error
properties of the snapshot to visually reflect the stream’s error on the screen. When it comes to pure Dart though, we have no widgets to work with so we have to go about doing this in a different way. Thankfully, Stream
class has a function called handleError()
that comes in handy.
The handleError()
function allows you to pass a function with either 1 or two parameters in, and react to errors that might occur in the stream. Let’s have a look at an example:
// the same stream as we had before Stream<String> getNames() async* { yield 'Vandad'; await Future.delayed(const Duration(seconds: 1)); yield 'John'; await Future.delayed(const Duration(seconds: 1)); throw 'Enough names for you'; } Future<void> main() async { // call the handleError() function with 2 // parameters to get both the error and the // stack trace final stream = getNames().handleError((error, stackTrace) { print('Error: $error'); }); await for (final name in stream) { print(name); } }
In this example we are taking the value of getNames()
function and calling the handleError()
function on it. The parameter we are passing to handleError()
itself has to be a function of either 1 or 2 parameters. If you pass a function with 2 parameters to handleError()
, you will get both the error and the stack-trace for the error, but if you pass a function with only 1 parameter, you only get the error.
Here is an example of passing a function with 1 parameter to the handleError()
function:
final stream = getNames().handleError((error) { print('Error: $error'); });
When you call getNames()
directly, the data-type of the value returned is not Stream<String>
, but instead it is _ControllerStream<String>
simply because that’s the internal class that represents all streams returned by an asynchronous generator. If you then apply the handleError()
function on any data type that represents a Stream
, such as _ControllerStream
, you will then get a new data type called _HandleErrorStream
, in this case _HandleErrorStream<String>
. Both _HandleErrorStream
and _ControllerStream
are private classes that conform to Stream
(so they are essentially streams), and act like streams, but since their logic is private, they are concealed in private classes. All we need to do is that all these functions essentially return Stream
instances. If you are curious about how these private classes work, do a simple Google on Flutter’s GitHub page to find their source code and have a look at their internals.
Apart from using the handleError()
function like we did, you can also add your await for
syntax inside a try/catch block as shown here:
Future<void> main() async { try { await for (final name in getNames()) { print(name); } } catch (e) { print('Error: $e'); } }
This example achieves the same result as the previous one with handleError()
except that this one uses a simpler syntax of try
catch
block rather than functional way of handling errors. It’s up to you and your team to decide which way of handling stream errors fits you best.
Mapping Streams
If you have a Stream and you want to convert each element of that stream to another value which might even have a different data-type than the original Stream, use the map()
function on Stream
. Let’s say that you have a stream of strings as shown here:
Stream<String> getNames() async* { yield 'Vandad'; await Future.delayed(const Duration(seconds: 1)); yield 'John'; }
Now if you want to consume this Stream in Flutter and transform every element to uppercase, you could do that with your AsyncSnapshot
and its data
or requireData
properties like we have seen before. But then you would have to do that every time you have the same requirement. That’s not a very scalable way of handling this especially if you have to do this task often.
You could, however, map your stream using its map()
function which gets called for every element in the stream and allows you to convert that value to any other value of any other data type (though not a Future
or another Stream
). Nothing asynchronous in other words. The mapping has to be done synchronously and in place, and cannot be an asynchronous operation. Here is an example:
Future<void> main() async { // map every element in the stream of strings // to their uppercase version final upperCase = getNames().map( (name) => name.toUpperCase(), ); await for (final name in upperCase) { print(name); // VANDAD, JOHN } }
Even though this is a nice piece of code, it’s still not that portable. You could make this into a function like so too:
// you could have a function that does the mapping // for you and you could just reuse the function Stream<String> toUpperCase(Stream<String> input) => input.map( (name) => name.toUpperCase(), ); Future<void> main() async { final upperCase = toUpperCase(getNames()); await for (final name in upperCase) { print(name); // VANDAD, JOHN } }
This is still one step better but not good enough. What we need here to make this code truly reusable is an extension on Stream<String>
as shown here:
extension ToUpperCase on Stream<String> { Stream<String> toUpperCase() => map( (e) => e.toUpperCase(), ); } Future<void> main() async { await for (final name in getNames().toUpperCase()) { print(name); // VANDAD, JOHN } }
With our ToUpperCase
extension, we can take any stream that produces instances of String
and convert the stream so that all string instances returned by the toUpperCase()
function become uppercase.
The then()
function on Stream
is very similar to the same function on Future
: it allows you to synchronously convert every element of the Stream
to another value that can even of another datatype than the original stream’s data type. However, if you want to perform asynchronous work for every element produced by the stream, you need to do asynchronous mapping!
Asynchronous Mapping of Streams
The then()
function synchronously maps every element of the stream to another value but if you want to perform asynchronous work for every element of the stream, then you need to use asyncMap()
. The asyncMap()
function works just like map()
with the difference that the asyncMap()
function’s parameter can be either a raw value that is synchronously returned or it can be a Future<E>
where E
can be a data type that is unrelated to the original data type of the stream.
Let’s say that you have a stream that reports the current date and time to you every N number of seconds, where N is decided by the call site:
// Gets the current date and time // and returns it as DateTime with // delay of "delay" between each // element production Stream<DateTime> dateAndTime({ required Duration delay, }) => Stream.periodic( delay, (_) => DateTime.now(), );
Let’s say that your job is to report the current date and time to a backend end-point, and the function that does that reporting for you might sometimes throw an error. The function will return a Future<void>
where the future just completes if the reporting to our backend goes according to plan or it throws an exception in the Future
if things don’t go so well. Since we don’t have a backend in this article, we are going to write a fake function that randomly throws an error:
import 'dart:math' show Random; // a function that simulates a backend API call // and sometimes throws an exception Future<void> reportTimeToServer(DateTime dateTime) async { await Future.delayed(Duration(seconds: 1)); // random number between 0 and 1 if (Random().nextInt(2) == 0) { throw 'Failed to report'; } }
Imagine consuming the original Stream and every time that stream produces a new value, we want to report the date and time to our server with the aforementioned function. Since the process of reporting the value of our stream to the server is an asynchronous call by the reportTimeToServer()
function, we need to use the asyncMap()
function on our stream, and not the map()
function. Here is how you could go about implementing that:
Future<void> main() async { // start by getting the current date and time // every 1 second await for (final _ in dateAndTime( delay: Duration( seconds: 1, ), ).asyncMap( // then map every date and time asynchronously // to the result of the "reportTimeToServer()" // function that is "void" (dateAndTime) => reportTimeToServer( dateAndTime, ), )) { // in here the data reported to our for loop // function will be "void" because we are // mapping every DateTime element of the original // stream to the result of the "API" call print('Got result'); } }
Note how the original stream produced values of type DateTime
but when we did the asyncMap()
on that stream, the resulting stream became Stream<void>
. The reason behind that is that we are indeed mapping, meaning that we are not only converting the stream elements to another value, but to another value of another data-type all together. As a rule, when you are mapping, whether synchronously or asynchronously, the data and its data type might change and you need to account for that.
Now if you run this code, you might either immediately get an exception, or your code might run for a few cycles and the reportTimeToServer()
function might just return void
without throwing its exception but after a while it will throw an exception and your program will stop its execution with a backtrace that will look like this:
dart app.dart Got result Got result Got result Got result Got result Unhandled exception: Failed to report #0 reportTimeToServer (app.dart:21:5) <asynchronous suspension> #1 Stream.asyncMap.<anonymous closure>.add (dart:async/stream.dart:793:7) <asynchronous suspension>
Our app basically threw an exception which we have not caught and that stops the execution of the entire application. We have learnt how to handle exceptions inside a Stream but this one is a funky one! The exception is being thrown in the reportTimeToServer()
function, inside the asyncMap()
conversion. How can we handle this error. Well, thankfully, we have already learnt how to handle exceptions / errors inside Future
because the returnTimeToServer()
function returns a Future
, remember?
Let’s go ahead and remedy this exception by quietly returning a void
from our catchError()
function:
Future<void> main() async { await for (final _ in dateAndTime( delay: Duration( seconds: 1, ), ).asyncMap( (dateAndTime) => reportTimeToServer( dateAndTime, // by catching the error and returning nothing (void) // the resulting Future will simply continue with a void // returned by this catch block ).catchError((_) {}), )) { print('Got result'); } }
The way catchError()
works here is that it calls the function we provide to it, and returns the return value of that function inside the resulting Future<void>
. Since our function returns nothing, or void
, that void
is then returned by the original Future
and then into the Stream
from the asyncMap()
function. So what happens eventually is that every time our reportTimeToServer()
function throws its exception, our for-loop will get a void
just like it would with non-exception cases.
Can we change this somehow so that the for-loop gets a boolean value indicating whether the reportTimeToServer()
function actually succeeded or not? Indeed we can. Let’s have a look:
Future<void> main() async { // now that our stream will be a "Stream<bool>" // let's read the elements of the stream into // the "wasSuccessful" variable await for (final wasSuccessful in dateAndTime( delay: Duration( seconds: 1, ), ).asyncMap( (dateAndTime) => // first report the results to server reportTimeToServer( dateAndTime, ) // for successful cases, convert the void to true // so "Future<void>" becomes "Future<bool>" .then((_) => true) // then catch the error and convert it to false // so we keep our "Future<bool>" type .catchError((_) => false), )) { print('Was successful? $wasSuccessful'); } }
If you then execute your code, you might get results that look similar to this:
Was successful? false Was successful? true Was successful? true Was successful? false Was successful? true Was successful? true Was successful? false
Every time our reportTimeToServer()
function throws its exception, we are catching that exception and synchronously converting it to false
which then gets reported back into the original stream since we are inside the asyncMap()
function when all of this conversion happens. In cases of successful reporting of time to the server, where an exception is not thrown, we convert the void
that comes back from the aforementioned function into a true
value of type bool
so the Future<void>
is effectively converted to a Future<bool>
and since the Future<bool>
is returned from asyncMap()
, our Stream<DateTime>
will be converted to Stream<bool>
!
When it comes to asynchronous programming in Dart and Flutter, as you might have noticed, a lot of the work is about data-type conversion, how you go from one data-type into another and how also you need to leverage error handling to your advantage to get your desired data back from your streams and futures.
One thing which is worth mentioning is that if you have a Stream that produces an element every 3 seconds, and you are asynchronously mapping every element using asyncMap()
to a Future
that completes after 4 seconds, the resulting stream will produce 1 element every 7 seconds, the combined time required for the stream and the future to produce 1 element.
Asynchronous Expanding of Streams
We have looked at asynchronously mapping stream elements into a Future
using asyncMap()
but what if you want to convert every stream element to another stream? For that we have the asyncExpand()
function on Stream
.
In this example we are going to define a function that is a stream of integers, a counter if you will, first it will produce 0, then 1, after that 2 and so on and so forth. Then we will create a function that produces a Stream<String>
which takes in a single integer parameter and using that integer it will know how many strings to produce. This function will choose the strings amongst a constant array of names that we feed into it. Let’s go ahead and program the function that produces a Stream<int>
, a counter if you will:
// a counter stream that starts from the value of 0 // and ends when the call-site stops consuming it Stream<int> countStream() => Stream.periodic( Duration(seconds: 1), (i) => i + 1, // + 1 so we start from 1, not 0 );
Once we have this counter stream, we can go ahead and define our constant list of names like so:
// define a list of names that we can pick from const names = [ 'Vandad', 'Alice', 'Bob', ];
Now that we have the list of names, let’s go ahead and write the second function that takes in a number of names to produce and produces a Stream<String>
like so:
// a function that produces "howMany" number of // names by picking them from the "names" list // it will produce a new name every second until // it has produced "howMany" number of names Stream<String> getNames(int howMany) async* { for (var i = 0; i < howMany; i++) { await Future.delayed(Duration(seconds: 1)); if (i < names.length) { yield names[i]; } else { // if the given index is out of bounds, // then we throw an exception for the call-site // to handle throw 'Index out of bounds'; } } }
Note how when the index runs out of bounds of the names
constant list, our getNames()
function will throw an exception saying "Index out of bounds"
. This will be an important issue to fix later on but for now just be aware of this fact; we’ll address this later.
Now back to our main goal. Every time the countStream()
function produces a new integer in its stream, we need to transform that integer element into a new stream of strings that is produced by the getNames()
function. Since every element of the countStream()
has to asynchronously (think streams, asynchronous) be transformed into another stream (using the getNames()
function), we cannot use the map()
function! We need to use its asynchronous counterpart called asyncMap()
as shown here:
Future<void> main() async { // then we can use the "countStream" function // and then asyncExpand to transform every // value of the stream into a new stream that // is produced by the "getNames" function await for (final name in countStream().asyncExpand((count) => getNames(count))) { print(name); } }
The output of our program will be similar to that shown here:
Vandad Vandad Alice Vandad Alice Bob Vandad Alice Bob Unhandled exception: Index out of bounds #0 getNames (app.dart:28:7) <asynchronous suspension>
As you can see, first we get the 1st name, which is "Vandad"
since our countStream()
function’s first stream element will be 1 (note that + 1
we added to the contStream()
function, go back and have a look to see what I’m referring to), and the loop in getNames()
will know to get the names in the range of 0 to 1 (excluding index 1 of course) and that will be "Vandad"
. Then the counter stream will produce the value of 2, our getNames()
function will try to read the names at indexes of 0 and 1 (2 elements) and so on and so forth until we run out of names and produce an error.
So what can we do about that error? Well, we know already how to handle errors, so let’s go ahead and fix our code now:
Future<void> main() async { try { await for (final name in countStream().asyncExpand((count) => getNames(count))) { print(name); } } catch (e) { print('Error: $e'); print('We probably ran out of names'); } }
Keep in mind that in here we are placing the stream inside a try / catch block but you can also use the handleError()
function on Stream<T>
in order to handle your errors. Since we want the stream to simply stop producing new elements once it emits an exception, the easiest way to handle it is of course using a try / catch block.
Note that if your original stream takes 2 seconds to produce a new result and you asyncExpand()
every element of this stream to another stream which takes 3 seconds to produce each of its elements, then the resulting stream will take 5 seconds to produce each element. In other words, the resulting stream’s time to produce each element is the accumulation of time required for each element to be produced along the way until you get to the consumption of the resulting stream.
Conclusion
I hope that you enjoyed this article. I think I’ve covered most of the topics related to the basics of futures and streams but there is a lot left still to be covered. I will be adding to this article every now and then with new topics so stay tuned.
Hey Vandad… Thank you for your time and effort to help us. Your flutter 37 hrs course really helped me. I am trying to add more functionality like users could create notes without signup and when they create their account then their notes are backed up from SQL to firestore.
Hi Hussain, that’s great to hear that you’re adding functionality at your own time
very good!
Great to hear that you like this article 🙂
Great content, Vandad! Keep going!
Thank you Cindi 😊
Thanks a lot for your effort and especially for class on bloc although I still have a lot of questions but am trying to figure them out, also I want to ask I know am not paying you and I don’t even have the money to pay but can you do a course on flutter animation and an article on dependency injection
Sure I can put that on the list
Thank you Vandad for that valuable content.
It’s my pleasure