is it possible to make the AsyncMemoizer cached by key - flutter

I am using AsyncMemoizer to run once like this in the FutureBuilder:
AsyncMemoizer _memoization = AsyncMemoizer<String>();
Future<String> initArticle(int id) async {
return await this._memoization.runOnce(() async {
Item? articleWithContent = await Repo.fetchArticleDetail(id);
if (articleWithContent != null) {
article = articleWithContent;
return articleWithContent.id;
}
return "0";
});
}
Now the this._memoization.runOnce code block only run once, but what I want to do only run once with the same article id. how to make the AsyncMemoizer run once with the article id? I have read the AsyncMemoizer source code but did not figure out what to do.

Related

how to avoid the flutter request server flood

I am using future builder to load some data from the server side, now I found this component will send reqeust to the server side every time refresh, this is my flutter code looks like:
return FutureBuilder(
future: articleDetailController.initArticle(int.parse(article.id)),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
articleDetailController.article = snapshot.data;
return new ArticleDetail();
} else {
return Center(child: CircularProgressIndicator());
}
});
when render this component, the reqeust will trigger so much times, how to avoid the reqeust flood to make the request send only once when load the article detail? this is the initial article function:
Future<Item> initArticle(int id) async {
Item? articleWithContent = await Repo.fetchArticleDetail(id);
if (articleWithContent != null) {
return articleWithContent;
}
return new Item();
}
I have tried to use AsyncMemoizer runonce in the async package but facing a new problem that it only run once with different article id, I want it changed with the article id. I define AsyncMemoizer the like this:
AsyncMemoizer _memoization = AsyncMemoizer<Item>();
and the get article code like this:
Future<Item> initArticle(int id) async {
return await this._memoization.runOnce(() async {
Item? articleWithContent = await Repo.fetchArticleDetail(id);
if (articleWithContent != null) {
return articleWithContent;
}
return new Item();
});
}
this code only load the server api for once, could not load each article by the article id. I also tried to use a flag to control the render invoke action:
Future<Item> initArticle(int id) async {
if (!run) {
run = true;
Item? articleWithContent = await Repo.fetchArticleDetail(id);
run = false;
if (articleWithContent != null) {
return articleWithContent;
}
}
return Future.value(new Item());
}
but the FutureBuilder seem did not render the article that returned from server.
Finally I am using synchronized package to avoid this problem, this code look like this:
Future<String> initArticle(int id) async {
return await lock.synchronized(() async {
// Only this block can run (once) until done
// https://stackoverflow.com/questions/74194103/how-to-avoid-the-flutter-request-server-flood
Item articleWithContent = await Repo.fetchArticleDetail(id);
article = articleWithContent;
return articleWithContent.id;
});
}
import this package and initial like this:
import 'package:synchronized/synchronized.dart';
var lock = new Lock();
By the way, do not forget to cache the article in the deep level of your request function, it still need to load multiple times, the first time fetch from server, and fetched from cache in the rest of request.

How to return a List, after a Method fills it, Flutter

I'm stuck with a problem and I wondered if you can help me.
I have a functions (in Flutter) that returns a List of Items. Now this List of Items should be Filled by an other function, which goes thought my Database and collect the right items. My Problem is, that my Function runs after the Return Statement... Here is some Code:
Future<List<MaterialItem>> getItems(String path, String fach) async {
// This is a empty List that I want to fill
List<MaterialItem> list = [];
// That's my Function, that fills the List
var result = await _db
.collection("$path/$fach/")
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
// Here the List gets filled
list.add(MaterialItem.fromSnapshot(doc.data() as Map<String, dynamic>));
});
});
// Here the List should be returned, but after my Function fills it.
return list;
}
Hope you know what my problem is, and someone can help me.
I think you could solve this using a Completer. Your function should return the Future property of the Completer and the database call should then complete it.
Take a look at the API and the example:
https://api.dart.dev/stable/2.12.4/dart-async/Completer-class.html
For example: (pseudo code)
Future<List<MaterialItem>> getItems(String path, String fach) async {
// declare a completer
Completer<List<MaterialItem>> completer = Completer();
List<MaterialItem> list = [];
final result = await _db
.collection("$path/$fach/")
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
list.add(MaterialItem.fromSnapshot(doc.data() as Map<String, dynamic>));
});
// let the database call complete the completer
completer.complete(list);
});
// return the future value of the completer
return completer.future;
}

Riverpod giving a bad state exception when one hits back button on webpage

I'm getting this error in my StateNotifiers when one hits the back button on their webpage. I've isolated it to happening where the longRunningAPI request is below.
Exception has occurred.
"Error: Bad state: Tried to use RunListNotifier after `dispose` was called.
and I have code like this.
final runListController = StateNotifierProvider.autoDispose
.family<RunListNotifier, AsyncValue<List<Run>>, RunListParameter>(
(ref, param) {
return RunListNotifier(read: ref.read, param: param);
});
class RunListNotifier extends StateNotifier<AsyncValue<List<Run>>> {
RunListNotifier({required this.read, required this.param})
: super(AsyncLoading()) {
fetchViaAPI(param);
}
final Reader read;
final RunListParameter param;
void fetchViaAPI(RunListParameter param) async {
state = AsyncLoading();
try {
List<Run> stuff = await read(apiProvider).longRunningAPI(param: param);
state = AsyncData(stuff);
} catch (e) {
state = AsyncError(e);
}
}
}
is it safe to simply do something like this in the catch?
} catch (e) {
if (e.runtimeType.toString() == 'StateError') {
// ignore the error
} else {
state = AsyncError(e);
}
}
I believe you could solve this problem by checking mounted before setting the state after your API call like so:
List<Run> stuff = await read(apiProvider).longRunningAPI(param: param);
if (!mounted) return;
state = AsyncData(stuff);
This simply checks if dispose was called and if so, don't attempt to modify the state.
Another resource that could be useful is adding a cancelToken to your API call and canceling if the provider is disposed.
final longRunningApi = FutureProvider.autoDispose.family<List<Run>, RunListParameter>((ref, param) async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
final api = await ref.watch(apiProvider);
final res = await api.longRunningApi(param, cancelToken);
ref.maintainState = true;
return res;
});
Then you'd have to add the cancelToken to your actual request. A great example of this in the marvel example project by the author of Riverpod can be found here.

Why getting the Instance of SharedPreferences is an async function? Is it a good practice to cache the instance?

I wonder why is SharedPreferences.getInstance() an async function?! I want to cache the instance in a static variable and use it to store settings data without having to use await or SharedPreferences.getInstance().then(...), but if they made it async, it should be for a good reason, any ideas?
SharedPreferences.getInstance() actually fetches the data and does not only provide a reference to a SharedPreferences's instance
based on the source code
static Future<SharedPreferences> getInstance() async {
if (_completer == null) {
_completer = Completer<SharedPreferences>();
try {
final Map<String, Object> preferencesMap =
await _getSharedPreferencesMap();
_completer.complete(SharedPreferences._(preferencesMap));
} on Exception catch (e) {
// If there's an error, explicitly return the future with an error.
// then set the completer to null so we can retry.
_completer.completeError(e);
final Future<SharedPreferences> sharedPrefsFuture = _completer.future;
_completer = null;
return sharedPrefsFuture;
}
}
return _completer.future;
}
and
static Future<Map<String, Object>> _getSharedPreferencesMap() async {
final Map<String, Object> fromSystem = await _store.getAll();
assert(fromSystem != null);
// Strip the flutter. prefix from the returned preferences.
final Map<String, Object> preferencesMap = <String, Object>{};
for (String key in fromSystem.keys) {
assert(key.startsWith(_prefix));
preferencesMap[key.substring(_prefix.length)] = fromSystem[key];
}
return preferencesMap;
}
I don't know if this was trivial, but it wasn't for me, I thought data are being fetched only on get functions.

I need some guidance in the Future Asynchronous Calls with flutter and dart, sometimes things happen out of order

The following code works fine, because it return only a simple list, but in some cases that I need to do nested Firebase calls, I can't make things happen in the right order, and the main return statement comes incomplete. What can I do to improve my Future Asynchronous Calls?
Future<List<MyNotification>> getNotifications() async {
var uid = await FirebaseAuth.instance.currentUser();
List<MyNotification> tempNots = await Firestore.instance
.collection("notifications")
.where("targetUsers", arrayContains: uid.uid)
.getDocuments()
.then((x) {
List<MyNotification> tempTempNots = [];
if (x.documents.isNotEmpty) {
for (var not in x.documents) {
tempTempNots.add(MyNotification.fromMap(not));
}
}
return tempTempNots = [];
});
return tempNots;
}
The most important thing; don't use then inside your async functions. I modified your code like this;
Future<List<MyNotification>> getNotifications() async {
// Using the type definition is better.
FirebaseUser user = await FirebaseAuth.instance.currentUser();
// The return type of getDocuments is a QuerySnapshot
QuerySnapshot querySnapshot = await Firestore.instance
.collection("notifications")
.where("targetUsers", arrayContains: user.uid)
.getDocuments();
List<MyNotification> tempTempNots = [];
if (querySnapshot.documents.isNotEmpty) {
for (DocumentSnapshot not in querySnapshot.documents) {
tempTempNots.add(MyNotification.fromMap(not));
}
}
return tempTempNots;
}