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
.