Home > OS >  Most elegant way to wait for async initialization in Dart
Most elegant way to wait for async initialization in Dart

Time:05-29

I have a class that is responsible for all my API/Database queries. All the calls as well as the initialization of the class are async methods.

The contract I'd like to offer is that the caller has to call [initialize] as early as possible, but they don't have to await for it, and then they can call any of the API methods whenever they need later.

What I have looks roughly like this:

class MyApi {

  late final ApiConnection _connection;
  late final Future<void> _initialized;

  void initialize(...) async {
    _initialized = Future<void>(() async {
      // expensive initialization that sets _connection
    });
    await _initialized;
  }

  Future<bool> someQuery(...) async {
    await _initialized;
    // expensive async query that uses _connection
  }

  Future<int> someOtherQuery(...) async {
    await _initialized;
    // expensive async query that uses _connection
  }

}

This satisfies the nice contract I want for the caller, but in the implementation having those repeated await _initialized; lines at the start of every method feel very boilerplate-y. Is there a more elegant way to achieve the same result?

CodePudding user response:

Short of using code-generation, I don't think there's a good way to automatically add boilerplate to all of your methods.

However, depending on how _connection is initialized, you perhaps instead could change:

  late final ApiConnection _connection;
  late final Future<void> _initialized;

to something like:

  late final Future<ApiConnection> _connection = _initializeConnection(...);

and get rid of the _initialized flag. That way, your boilerplate would change from:

  Future<bool> someQuery(...) async {
    await _initialized;
    // expensive async query that uses `_connection`

to:

  Future<bool> someQuery(...) async {
    var connection = await _connection;
    // expensive async query that uses `connection`

This might not look like much of an improvement, but it is significantly less error-prone. With your current approach of using await _initialized;, any method that accidentally omits that could fail at runtime with a LateInitializationError when accessing _connection prematurely. Such a failure also could easily go unnoticed since the failure would depend on the order in which your methods are called. For example, if you had:

Future<bool> goodQuery() async {
  await _initialized;
  return _connection.doSomething();
}

Future<bool> badQuery() async {
  // Oops, forgot `await _initialized;`.
  return _connection.doSomething();
}

then calling

var result1 = await goodQuery();
var result2 = await badQuery();

would succeed, but

var result2 = await badQuery();
var result1 = await goodQuery();

would fail.

In contrast, if you can use var connection = await _connection; instead, then callers would be naturally forced to include that boilerplate. Any caller that accidentally omits the boilerplate and attempts to use _connection directly would fail at compilation time by trying to use a Future<ApiConnection> as an ApiConnection.

  • Related