Dart - Using Future Completer Examples

This tutorial shows you how to use Completer in Dart programming language, which also works for Flutter framework.

If you're using Dart or developing a Flutter application, you may already be familiar with Future. Future is usually used to handle asynchronous tasks. Getting the result of a Future is quite simple. For example, if we have a function Future fetchResult(), it's possible to get the result by using the await keyword.

  String result = await fetchResult();

Another alternative is by using a Future chain.

  fetchResult()
    .then((value) {
      print(value);
    });

However, in some cases, it's impossible to use the methods above to create a Future. For example, if you need to get the result of a Future from a callback function. Let's take a look at the example below. There is a class MyExectuor that has a method run. The class's constructor has a required callback onDone. The run method will call the onDone method with a value. The objective here is to get the value passed to the onDone callback.

  import 'dart:async';
  
  class MyExecutor {
  
    void Function(String time) onDone;
  
    MyExecutor({
      required this.onDone,
    });
  
    Future<void> run() async {
      await Future.delayed(Duration(seconds: 2));
      final DateTime time = DateTime.now();
      final String formattedTime = '${time.year}-${time.month}-${time.day} ${time.hour}:${time.minute}:${time.second}';
      onDone(formattedTime);
    }
  }
  
  main() async {
    final MyExecutor myExecutor = MyExecutor(
      onDone: (String time) async {
        // we want to get the value when this callback is invoked
      }
    );

    await myExecutor.run();

  }
  

Since the run method returns void, which means it doesn't return anything, we cannot get the value directly by using await.run(). Instead, we have to get the value inside the passed callback. A possible solution is by using a Completer.

Using Completer

First, you need to create a Completer by calling its constructor. It has a type parameter where you can define the return type. Below is an example of a Completer that returns a string.

  final stringCompleter = Completer<String>();

If it doesn't have a return value, you can create a Completer with void parameter type.

  final voidCompleter = Completer<>();

If you don't specify the type parameter, it will be set as dynamic.

  final dynamicCompleter = Completer(); // Completer<dynamic>

You can also use a nullable type if the return value can be null.

  final nullableStringCompleter = Completer<String?>();

Complete Future

To complete the Future, you can call the complete method with a return value. The value type must be the same as the type parameter when creating the Completer. If the type parameter is void, you don't need to pass any value. If it's dynamic, you can return a value of any type. You are only allowed to return null if the type parameter is void or nullable.

  void complete([FutureOr<T>? value]);

Below is an example of how to complete the Future with a string value.

  stringCompleter.complete('foo');

The complete method can only be called once. If the Future already completes and you call the complete method again, it will throw StateError.

Complete Future with Error

A Future can also throw an error. With Completer, throwing an error can be done by calling completeError method.

  void completeError(Object error, [StackTrace? stackTrace]);

Here is an example of how to call the completeError method.

  try {
    // Do something
  } catch (ex, stackTrace) {
    stringCompleter.completeError(ex, stackTrace);
  }

Calling complete or completeError must be done no more than once.

Get Future and Value

If you have a Completer instance, you can access the future property to get the Future that's completed when complete or completeError is called. If you've got the Future, you can use the await keyword to get the value returned by the Future.

  final valueFuture = stringCompleter.future;
  final value = await valueFuture;

Get Completion Status

To get the completion status of the Future, you can access the isCompleted property.

  final isCompleted = stringCompleter.isCompleted;

Full Example

  import 'dart:async';
  
  class MyExecutor {
  
    void Function(String time) onDone;
  
    MyExecutor({
      required this.onDone,
    });
  
    Future<void> run() async {
      await Future.delayed(Duration(seconds: 2));
      final DateTime time = DateTime.now();
      final String formattedTime = '${time.year}-${time.month}-${time.day} ${time.hour}:${time.minute}:${time.second}';
      onDone(formattedTime);
    }
  }
  
  main() async {
    final stringCompleter = Completer<String>();
    // final nullableStringCompleter = Completer<String?>();
    // final voidCompleter = Completer<String>();
    // final dynamicCompleter = Completer();
  
    final MyExecutor myExecutor = MyExecutor(
      onDone: (String time) async {
        try {
          if (time.isNotEmpty) {
            stringCompleter.complete(time);
          } else {
            throw Exception('empty time');
          }
        } catch (ex, stackTrace) {
          stringCompleter.completeError(ex, stackTrace);
        }
      }
    );
  
    print('isCompleted: ${stringCompleter.isCompleted}');
  
    await myExecutor.run();
  
    print('isCompleted: ${stringCompleter.isCompleted}');
    final valueFuture = stringCompleter.future;
    final value = await valueFuture;
    print('value: $value');
  }
  

Summary

Completer can be used as an alternative to create a Future in Dart/Flutter. The usage is quite simple. You need to create a Completer, then call the complete or completeError method. You can get the Future of a Completer from the future property. The isCompleted property can be used to know whether the Future has been completed.