Using Futures in Flutter

Saša Ivičević, Senior Software Developer

Tech

30.09.2020.

featured image

Long-running tasks or asynchronous operations are common in mobile apps. For example, these operations can be fetching data over network, writing to database, reading data from a file, etc.

To perform such operations in Flutter/Dart, we usually use a Future class and the keywords async and await. A Future class allows you to run work asynchronously to free up any other threads that should not be blocked. Like the UI thread.

Define a Future

Let’s start with real-life analogies so that we can better understand the real purpose of Futures. Basically, you can think of Futures as little gift boxes for data. Somebody hands you one of these gift boxes, which starts off closed. A little while later the box pops open, and inside there’s either a value or an error.

Technically speaking: A Future represents a computation that doesn’t complete immediately. Whereas a normal function returns the result, an asynchronous function returns a Future, which will eventually contain the result. The Future will tell you when the result is ready.

So, a Future can be in one of 3 states:

  1. Uncompleted: The gift box is closed.
  2. Completed with value: The box is open, and your gift (data) is ready.
  3. Completed with an error: The box is open, but something went wrong. 
Three states of a Future in Flutter
Three states of a Future

A Future is defined exactly like a function in Dart, but instead of Void you use Future. If you want to return a value from Future, then you pass it a Type.

Future myFutureAsVoid() {} 
Future myFutureAsType() {}

Best way to explain the usage of Futures is to use a real-life example. So, in the following code example, fetchUserOrder() returns a Future that completes after printing to the console. Because it doesn’t return a usable value, fetchUserOrder() has the type Future.

Future fetchUserOrder() {
  // Imagine that this function is fetching user info from another service or database.
  return Future.delayed(Duration(seconds: 2), () => print('Large Latte'));
}
 void main() {
  fetchUserOrder();
  print('Fetching user order...');
}

As you can see, even though fetchUserOrder() executes before the print() call, the console will show the output “Fetching user order…” before the output from fetchUserOrder(): “Large Latte”. This is because fetchUserOrder() delays before it prints “Large Latte”.

Using a Future

There are two ways to execute a Future and use the value it returns. If it returns any at all. The most common way is to await on the Future to return. For this to work, your function that’s calling the code has to be marked async.

Future getProductCostForUser() async {
  var user = await getUser();
  var order = await getOrder(user.uid);
  var product = await getProduct(order.productId);
  return product.totalCost;
}
 main() async {
  var cost = await getProductCostForUser();
  print(cost);
}

When an async function invokes await, it is converted into a Future, and placed into the execution queue. When the awaited Future is complete, the calling function is marked as ready for execution and it will be resumed at some later point because the value of what was awaited is contained within a Future object.
The important difference is that no Threads need to be paused in this model.

In other words, async-await is just a declarative way of defining asynchronous functions and using their results into Future and it provides syntactic sugar that helps you write clean code involving Futures.

Here’s a thing to remember! If await is going to be used, we need to make sure that both the caller function and any functions we call within that function use the async modifier.

Sometimes you don’t want to turn the function into a Future or mark it async, so the other way to handle a Future is by using the .then function. It takes in a function that will be called with the value type of your Future. It’s similar to a Promise in JavaScript without the resolve, reject explicitness.

void main() {
  Future.delayed(
    const Duration(seconds: 3),
    () => 100,
  ).then((value) {
    print('The value is $value.'); // Prints later, after 3 seconds.
  });
  print('Waiting for a value...'); // Prints first.
}

Here’s the output of the preceding code:

Waiting for a value... (3 seconds pass until callback executes)
The value is 100.

In addition to executing your code, then() returns a Future of its own, matching the return value of whatever function you give it. 

Error Handling

Futures have its own way of handling errors. In the .then call, in addition to passing in your callback you can also pass in a function to onError that will be called with the error returned from your Future.

// ui code
FlatButton(
  child: Text('Run My Future'),
  onPressed: () {
    runFuture();
  },
)
 // Future
 Future myFutureAsType() async {
  await Future.delayed(Duration(seconds: 1));
  return Future.error('Error from return!');
}
 // Function to call future
 void runFuture() {
  myFutureAsType().then((value) {
    // Run extra code here
  }, onError: (error) {
    print(error);
  });

If you run the code above, you’ll see the ‘Error from return!’ message printed out after 1 second. If you want to explicitly handle and catch errors from the Future, you can also use a dedicated function called catchError.

void runFuture() {
  myFutureAsType().then((value) {
    // Run extra code here
  })
  .catchError( (error) {
    print(error);
  });
}

When handling an error in your Future you don’t need to always return the Future.error. Instead, you can also just throw an exception and it’ll arrive at the same .catchError or onError callback.

Future myFutureAsType() async {
    await Future.delayed(Duration(seconds: 1));
    throw Exception('Error from Exception');
  }

You can also mix await and .catchError. You can await a Future and use the .catchError call instead of wrapping it. This way, the value returned is null but you have the opportunity to handle the error as well without wrapping it in try/catch.

Future runMyFuture() async {
    var value = await myFutureAsType()
    .catchError((error) {
      print(error);
    });
  }

Managing Multiple Futures at Once

Let’s take an example where you have a screen where you can tap to download various items out of a list. You want to wait for all these Futures to be complete before you continue with your code. Future has a handy .wait call. This call allows you to provide a list of Futures to it and it will run all of them. When the last one is complete, it will return context to your current Future.

// ui to call futures
FlatButton(
  child: Text('Run Future'),
  onPressed: () async {
    await runMultipleFutures();
  },
)
// Future to run
Future myFutureAsType(int id, int duration) async {
  await Future.delayed(Duration(seconds: duration));
  print('Delay complete for Future $id');
  return true;
}
// Running multiple futures
Future runMultipleFutures() async {  // Create list of multiple futures
  var futures = List();
  for(int i = 0; i < 10; i++) {
    futures.add(myFutureAsType(i, Random(i).nextInt(10)));
  } 
  // Waif for all futures to complete
  await Future.wait(futures); 
  // We're done with all futures execution
  print('All the futures has completed');
}

If you tap the flat button above, we will kick of ten Futures all together and wait for all of them to complete. You should see a result similar to the one illustrated below. It’s using a random generator, so you’ll see different orders of the IDs.

I/flutter (12116): Delay complete for Future 7
I/flutter (12116): Delay complete for Future 3
I/flutter (12116): Delay complete for Future 9
I/flutter (12116): Delay complete for Future 4
I/flutter (12116): Delay complete for Future 2
I/flutter (12116): Delay complete for Future 1
I/flutter (12116): Delay complete for Future 8
I/flutter (12116): Delay complete for Future 0
I/flutter (12116): Delay complete for Future 6
I/flutter (12116): Delay complete for Future 5
I/flutter (12116): All the futures has completed

Timeouts

Sometimes we don’t know exactly how long a Future will run. It is a process that the user has to explicitly wait for, i.e. there’s a loading indicator on the screen, so you probably don’t want it to run for too long. In case you have something like this, you can timeout a Future using the timeout call.

Future myFutureAsType(int id, int duration) async {
    await Future.delayed(Duration(seconds: duration));
    print('Delay complete for Future $id');
    return true;
  } 
   Future runTimeout() async {
    await myFutureAsType(0, 10)
        .timeout(Duration(seconds: 2), onTimeout: (){
          print('0 timed out');
          return false;
        });
  }

If you run the code above, you’ll see 0 timed out and you'll never see Delay complete for Future 0. You can add additional logic into the onTimeout callback. That covers the basics of what you'd need to handle Futures in your code. There's also the function .asStream that you can on use a Future to return the results into a stream. If you have a code base dominated by streams, you can make use of this and merge it with your other streams easily if required.

References: 

Medium

Dart.Dev

Dev.to

Medium - Flutter Community 

RELATED

23.09.2020.

Metrics, Logging and Tracing Are Just Data

Usually we think of logging and tracing data as continuously updated long chronologically ordered lines or records organized in plain text files with a relatively simple purpose – to keep record of the operating system, network, application or of service events and activities. 

Read more