Most elegant way to wait for async initialization in Dart - flutter

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?

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

Flutter Riverpod Future provider - requires async and await?

I've been reviewing the RiverPod 2 tutorial at https://codewithandrea.com/articles/flutter-state-management-riverpod/
In the section dealing with Future providers there is a code snippet as shown below...
final weatherFutureProvider = FutureProvider.autoDispose<Weather>((ref) {
// get repository from the provider below
final weatherRepository = ref.watch(weatherRepositoryProvider);
// call method that returns a Future<Weather>
return weatherRepository.getWeather(city: 'London');
});
I can't understand why this code snippet is missing the 'async' and 'await' syntax as shown below...
final weatherFutureProvider = FutureProvider.autoDispose<Weather>((ref) async {
// get repository from the provider below
final weatherRepository = ref.watch(weatherRepositoryProvider);
// call method that returns a Future<Weather>
return await weatherRepository.getWeather(city: 'London');
});
Is my version correct or what?
Think of it as doing:
Future<int> example() {
return Future.value(42);
}
instead of:
Future<int> example() async {
return await Future.value(42);
}
Sure, you can use async/await. But it is technically optional here.
Doing return future vs return await future doesn't change anything. In fact, there's a lint for removing the unnecessary await: unnecessary_await_in_return
The async keyword is generally helpful. It catches exceptions in the function and converts them into a Future.error.
But FutureProvider already takes care of that. So async could also be omitted

How to provide Future to inside Providers using Riverpod?

I'm trying to learn Riverpod with clean architecture.
I have following set/chain of providers:
final databaseFutureProvider = FutureProvider<Database>((ref) async {
final db = await DatabaseHelper().createDatabase(); // this is async because 'openDatabase' of sqflite is async
return db;
});
final toDoDatasourceProvider = Provider<ToDoDatasource>((ref) {
final db = ref.watch(databaseFutureProvider); // problem is here!!
return ToDoDatasourceImpl(db: db);
});
final toDoRepositoryProvider = Provider<ToDoRepository>((ref) {
final ds = ref.watch(toDoDatasourceProvider);
return ToDoRepositoryImpl(ds);
});
I am probably missing some small things or doing it completely wrong. How to properly provide DB (that is async in its nature)?
You don't need multiple providers, in your case since you would need ToDoRepository always you can just Initialized before running the app and use it later without worrying about the database connection state
Future<void> main(List<String> args) async {
// Initialization the db
final db = await DatabaseHelper().createDatabase();
ProviderScope(
overrides: [
// pass the db
toDoRepositoryProvider.overrideWithValue(db),
],
child: RootApp(),
);
}
final toDoRepositoryProvider = Provider<ToDoRepository>((ref) {
throw UnimplementedError();
});
I totally agree with Mohammed Alfateh's decision. In addition, you can use ProviderContainer()..read(toDoDatasourceProvider) and UncontrolledProviderScope, to asynchronously assign values in main method. And in ToDoDatasourceImpl call the async method init() to assign a value in the field late final db.

Does compute support asnc request?

I have a list of String address like:
List<String> addressStrings = [....];
I am using geocoding plugin to get the address data and marker for these address strings like:
//This is a class-level function
Future<List<MarkerData>> getMarkerDataList() async {
List<MarkerData> list = [];
addressStrings.forEach((element) async {
final result = await locationFromAddress(element);
final markerData = MarkerData(element, result.first);
list.add(markerData);
});
return list;
}
But it freezes the UI as expected. I tried to use compute to perform the operation in another isolate like:
//This is a top-level function
Future<List<MarkerData>> getMarkerDataList(List<String> addressStrings) async {
List<MarkerData> list = [];
addressStrings.forEach((element) async {
final result = await locationFromAddress(element);
final markerData = MarkerData(element, result.first);
list.add(markerData);
});
return list;
}
//This is a class-level function
Future<List<MarkerData>> getMarkerData()async{
final result = await compute(getMarkerDataList, addressStrings);
return result;
}
But it doesn't work and shows Unhandled exception in the console.
I guess final result = await locationFromAddress(element); request is the problem here. Because it do pass before that statement but doesn't this one.
So, my question is: does compute support async? If yes, what I am doing wrong here? If no, how can I do asynchronous performance intensive tasks like this efficiently without blocking the UI?
Yes, as far as I know async does support compute - here's an article that should help out:
https://medium.com/flutterdevs/flutter-performance-optimization-17c99bb31553

How do I resolve a Future<File>, transform it to File, and ensure it's available before runApp starts?

My apologies for what I assume is rather basic question, but I'm struggling to understand this. I'm aware of What is a Future and how do I use it? but I don't think that applies in this case, or if it does I'm even more confused than I thought!
I'm attempting to use FileOutput in the Logger package to log to device storage. This requires a File object as a parameter.
To obtain the correct path I'm using getApplicationDocumentsDirectory() from the path_provider package. This returns a Future which I can manipulate to a Future in an async function with await.
I'm unclear though how to extract a File from this and how to make sure that these objects are available to the logger before they are needed. Does this need to be done before I call runApp()? I assume I don't need to and shouldn't push async up to the main()?
This is where I am. x2() is a test function I can call successfully out of main() after invoking runApp() and gives me the correct results.
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
print("directory: $directory");
return directory.path;
}
Future<File> get _localFile async {
final path = await _localPath;
print("path: $path");
return File('$path/logger.out');
}
var logger2 = Logger(
output: MultiOutput([
ConsoleOutput(),
FileOutput(
file: _localFile,
overrideExisting: true,
),
]),
printer: PrettyPrinter(
printBox: false,
printTime: true,
),
filter: ProductionFilter(),
);
void x2() async {
var f = await _localFile;
print("_localFile: $f");
}
You should be able to set the Logger before the runApp but you don't need to await for it you can create a logger then access with a provider or a singleton instance and you can simple have a getLogger method that checks if the instance is available and if not calls the await overthere
this way you only call the future once and cache the instance in the class.
You may initialise your variable in x2() function.
Also, don't forget to await x2() as it will assure you that your instance has been created.
class Logger{
Logger({required this.number});
int number;
}
Future<int> get someNumber => Future.delayed(Duration(seconds:1),()=>5);
var logger;
Future<void> x2() async {
logger=Logger(number: await someNumber);
print(logger.number);
}
void main()async{
await x2();
print("main :: "+ logger.number.toString());
}
edit:
Also as #ahmetakil suggested, use provider or inherited widget if you need this instance down the widget tree

Dart Flutter, help me understand futures

See this code:
class SomeClass{
String someVariable;
SomeClass();
Future<String> getData () async {
Response response = await get('http://somewebsite.com/api/content');
Map map = jsonDecode(response.body); // do not worry about statuscode, trying to keep it minimal
someVariable = map['firstName'];
return 'This is the first name : $someVariable';
}
}
Now look at main:
void main(){
String someFunction() async {
SomeClass instance = SomeClass(); // creating object
String firstNameDeclaration = await instance.getData().then((value) => value);
return firstNameDeclaration;
}
}
When working with Future, like in the case of firstNameDeclaration why do I have to use .then() method to access the string object, since I am waiting for the function to finish?
When searching on the web, some people use .then() others don't, I am confused.
Kindly help me have a clearer understanding of how Futures and async functions overall work.
Background
Asynchronous operations let your program complete work while waiting for another operation to finish. Here are some common asynchronous operations:
Fetching data over a network.
Writing to a database.
Reading data from a file.
To perform asynchronous operations in Dart, you can use the Future class and the async and await keywords.
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. The important difference is that no Threads need to be paused in this model.
Futures vs async-await
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. The important difference is that no Threads need to be paused in this model.
async-await is just a a declarative way to define asynchronous functions and use their results into Future and it provides syntactic sugar that help you write clean code involving Futures.
Consider this dart code snipped involving Futures -
Future<String> getData(int number) {
return Future.delayed(Duration(seconds: 1), () {
return 'this is a future string $number.';
});
}
main(){
getData(10).then((data) => {
print(data)
});
}
As you can see when you use Futures, you can use then callback when the function return a future value. This is easy to manage if there is single "then" callback but the situation escalates quickly as soon as there are many nested "then" callbacks for example -
Future<String> getProductCostForUser() {
return getUser().then((user) => {
var uid = user.id;
return getOrder(uid).then((order) => {
var pid = order.productId;
return getProduct(pid).then((product) => {
return product.totalCost;
});
});
});
}
main(){
getProductCostForUser().then((cost) => {
print(cost);
});
}
As you can when there multiple chained "then" callback the code become very hard to read and manage. This problem is solved by "async-await". Above chained "then" callbacks can be simplified by using "async-await" like so -
Future<String> 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);
}
As you can above code is much more readable and easy to understand when there are chained "then" callbacks.
I hope this explains some basic concepts and understanding regarding the "async-await" and Futures.
You can further read about topic and examples here
Basically, you should either use await OR then(). However, Dart guidelines advocates that you should prefer use await over then() :
This code :
Future<int> countActivePlayers(String teamName) {
return downloadTeam(teamName).then((team) {
if (team == null) return Future.value(0);
return team.roster.then((players) {
return players.where((player) => player.isActive).length;
});
}).catchError((e) {
log.error(e);
return 0;
});
}
should be replaced by :
Future<int> countActivePlayers(String teamName) async {
try {
var team = await downloadTeam(teamName);
if (team == null) return 0;
var players = await team.roster;
return players.where((player) => player.isActive).length;
} catch (e) {
log.error(e);
return 0;
}
}
In your case, you should write :
void main(){
Future<String> someFunction() async {
SomeClass instance = SomeClass(); // creating object
String firstNameDeclaration = await instance.getData();
return firstNameDeclaration;
// Or directly : return await instance.getData();
// Or : return instance.getData();
}
}