How to properly implement pagination with firestore stream on flutter (in this case flutter web) ?
my current approach with bloc which is most likely wrong is like this
function called on bloc when load next page, notice that i increased the lastPage variable of the state by 1 each time the function is called:
Stream<JobPostingState> _loadNextPage() async* {
yield state.copyWith(isLoading: true);
try {
service
.getAllDataByClassPage(state.lastPage+1)
.listen((List<Future<DataJob>> listDataJob) async {
List<DataJob?> listData = [];
await Future.forEach(listDataJob, (dynamic element) async {
DataJob data= await element;
listData.add(data);
});
bool isHasMoreData = state.listJobPostBlock.length!=listData.length;
//Update data on state here
});
} on Exception catch (e, s) {
yield StateFailure(error: e.toString());
}}
function called to get the stream data
Stream<List<Future<DataJob>>> getAllDataByClassPage(
String className, int page) {
Stream<QuerySnapshot> stream;
if (className.isNotEmpty)
stream = collection
.orderBy('timestamp', "desc")
.where('class', "==", className).limit(page*20)
.onSnapshot;
else
stream = collection.onSnapshot;
return stream.map((QuerySnapshot query) {
return query.docs.map((e) async {
return DataJob.fromMap(e.data());
}).toList();
});
}
With this approach it works as intended where the data loaded increased when i load next page and still listening to the stream, but i dont know if this is proper approach since it replace the stream could it possibly read the data twice and end up making my read count on firestore much more than without using pagination. Any advice is really appreciated, thanks.
Your approach is not very the best possible indeed, and as you scale you going to be more costly. What I would do in your shoes would be to create a global variable that represents your stream so you can manipulate it. I can't see all of your code so I am going to be as generic as possible so you can apply this to your code.
First let's declare the stream controller as a global variable that can hold the value of your stream:
StreamController<List<DocumentSnapshot>> streamController =
StreamController<List<DocumentSnapshot>>();
After that we need to change your getAllDataByClassPage function to the following:
async getAllDataByClassPage(String className) {
Stream stream = streamController.stream;
//taking out of the code your className logic
...
if(stream.isEmpty){
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.limit(20)
.onSnapshot
streamController.add(snap.docs);
}else{
DocumentSnapshot lastDoc = stream.last;
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.startAfterDocument(lastDoc)
.limit(20)
.onSnapshot;
streamController.add(snap.docs);
}
}
After that all you need to do in order to get the stream is invoke streamController.stream;
NOTE: I did not test this code but this is the general ideal of what you should try to do.
You can keep track of last document and if has more data on the list using startAfterDocument method. something like this
final data = await db
.collection(collection)
.where(field, arrayContains: value)
.limit(limit)
.startAfterDocument(lastDoc)
.get()
.then((snapshots) => {
'lastDoc': snapshots.docs[snapshots.size - 1],
'docs': snapshots.docs.map((e) => e.data()).toList(),
'hasMore': snapshots.docs.length == limit,
});
Related
I'm reading json List from device memory and want to perform some operations on it's components.
When I load that list I start loop where I check each item of that list.
While in loop I add each item to new List to have updated List after loop ends so I could save it on device memory.
If some conditions are true then I use future async http call to get updated data
then theoretically I update that item of the List while staying inside loop. And thus after loop ends I must have updated Json List ready to be saved on device memory.
Problem is that While I http call inside loop, the answer delays, loop ends and new Json List is being constructed and saved on memory without the component that was supposed to be updated.
Is there any way to force wait the whole loop or something else ?
Here is the code
Future<void> readStoredData() async {
try {
final prefs = await SharedPreferences.getInstance();
_rawJsonListE = prefs.getStringList('storedData');
List<String> rawJsonListNEW = [];
bool _isNeedUpdate = false;
_rawJsonListE!.forEach((item) async {
if (someCondition with item Data) {
_isNeedUpdate = true;
await makeHttpCallFutureAwaitFunction(item).then((_) {
rawJsonListNEW.add(updatedItem);
});
} else {
rawJsonListNEW.add(item);
}
});
if (_isNeedUpdate) prefs.setStringList('storedData', rawJsonListNEW);
}
notifyListeners();
} catch (error) {
print('Error : ${error}');
throw error;
}
You can separate the refreshing data part to another function.
// Just need to check _rawJsonListE is empty or not
_isNeedUpdate = _rawJsonListE.isNotEmpty();
Create a new function.
Future<List<String>> checkDataAndRefresh(List<String> _rawJsonListE) async {
List<String> rawJsonListNEW = [];
_rawJsonListE!.forEach((item) async {
if (someCondition with item Data) {
final String newString = await makeHttpCallFutureAwaitFunction(item);
rawJsonListNEW.add(newString);
} else {
rawJsonListNEW.add(item);
}
});
return rawJsonListNEW;
}
And if _isNeedUpdate is true, do work.
if (_isNeedUpdate)
final List<String> newData = await checkDataAndRefresh(_rawJsonListE);
prefs.setStringList('storedData', newData);
int FriendsNum() {
_firestore.collection('Friends').doc(User.userID).collection("Friends").where("Status",isEqualTo: 2)
.get()
.then((res)=> return{res.size});
return 0;
}
I am basically trying to get the number of documents that are inside the collection that has the status of 2, and the value should be returned so it can be displayed for the user, what can be changed in the syntax to make this work? with many thanks!
You have to change your function return type to a Future and use the await keyword to get the result from the firestore collection as it is an asynchronous operation itself.
The updated code will be the following:
Future<int> FriendsNum() async {
final count = await _firestore
.collection('Friends')
.doc(User.userID)
.collection("Friends")
.where("Status",isEqualTo: 2)
.get()
.then((res) => res.size);
return count;
}
I'm using BehaviorSubject as a Stream controller.
In one of my functions, I want to .add more items only in case the Stream is empty of events.
#override
Future<void> fetchNextOverviewPolls() async {
if (await _pollOverviewStreamController.isEmpty) return; // My Problem
final lastDoc = await _pollOverviewStreamController.last;
final querySnapshot =
await _overviewPollsRef.startAfterDocument(lastDoc).limit(5).get();
for (final doc in querySnapshot.docs) {
_pollOverviewStreamController.add(doc);
}
}
The isEmpty property returns a value in case the Stream ends. I want to check it when the Stream is still running.
How do I do that?
BehaviorSubject supports hasValue.
In the above case, use this line instead:
if (_pollOverviewStreamController.hasValue) return;
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;
}
I am trying to get a document stream from Firestore, to extract one of its fields in my repository and to stream the field to my bloc. I use return orderBriefDoc.snapshots().map((snapshot) { for this.
However, upon first call, no internal map instruction becomes executed and instead I receive a type mismatch type '_MapStream<DocumentSnapshot, dynamic>' is not a subtype of type 'Stream<List<OrderBrief>>'. I do not understand why the return type of the .map() method does not depend on what I return within its return statement and why this internal code is not executed.
First of all, I used the repository function of Felix Angelov's Firebase authentication ant of the todo list as a blueprint:
Stream<User> get user {
return _firebaseAuth.authStateChanges().map((firebaseUser) {
return firebaseUser == null ? User.empty : firebaseUser.toUser;
});
}
Stream<List<Todo>> todos() {
return todoCollection.snapshots().map((snapshot) {
return snapshot.documents
.map((doc) => Todo.fromEntity(TodoEntity.fromSnapshot(doc)))
.toList();
});
}
My adaption looks like this
#override
Stream<List<OrderBrief>> orderBriefs() {
if (orderBriefDoc == null)
getOrderCollection();
return orderBriefDoc.snapshots().map((snapshot) {
final tmp = snapshot;
print ("2");
final tmp2 = snapshot.data()['orderBriefs'];
print("3");
return snapshot.data()['orderBriefs'].map((orderBrief) {
final tmp=orderBrief;
final tmp2 = OrderBriefEntity.fromMap(orderBrief);
final tmp3 = OrderBrief.fromEntity(tmp2);
return tmp3;
}).toList();
});
}
For some reason "2" and "3" are not printed upon first call, and due to the type mismatch the app execution fails. So in my function orderBriefs() I return a .map() of a snapshots() stream. The mapped value, so the single document snapshot is mapped again to extract the orderBriefs field. This field is transformed from an storage entity class OrderBriefEntity to my business logic class OrderBrief. The transformation result is the final return value. Hence I would expect the function orderBriefs() to return a list stream of this transformation result. However, a _MapStream<DocumentSnapshot, dynamic> is returned. Why?
PS: This refers to my question, but with a slightly different angle
So, finally I found a method to stream a single document of Firestore. I finally had the idea to look up the documentation of Stream and found a working example there. Why it does not work with only the map method like Felix did it, no idea. That being said, I still follow his pattern to transform the snapshot to an "entity" and then further to the data structure used by the bloc and the ui.
I finally needed to flavors, stream a single field (nested array) of a document and stream a whole document
(1) Streaming a field of a document.
#override
Stream<List<OrderBrief>> orderBriefs() async* {
if (orderBriefDoc == null)
getOrderCollection();
Stream<DocumentSnapshot> source = orderBriefDoc.snapshots();
await for (var snapshot in source) {
final List<OrderBrief> returnVal = snapshot.data()['orderBriefs'].map<OrderBrief>((orderBrief) {
return OrderBrief.fromEntity(OrderBriefEntity.fromMap(orderBrief));
}).toList();
yield returnVal;
}
}
(2) Streaming a document with all of its fields
#override
Stream<Order> orderStream(String orderId) async* {
Stream<DocumentSnapshot> source = orderBriefDoc.collection('orders').doc(orderId).snapshots();
await for (var snapshot in source) {
final Order returnVal = Order.fromEntity(OrderEntity.fromSnapshot(snapshot));
yield returnVal;
}
}
How to neatly integrate this with a listener in the bloc, you will find in the Firestore ToDos example at bloclibrary