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.