Merge Firestore's separate queries Streams in Dart - asynchronous

I'm implementing a Todo Application in Flutter. I need to merge a double query in client, in order to perform an OR request in Firestore.
One hand, I have the following code that performs the double queries.
Future<Stream> combineStreams() async {
Stream stream1 = todoCollection
.where("owners", arrayContains: userId)
.snapshots()
.map((snapshot) {
return snapshot.documents
.map((doc) => Todo.fromEntity(TodoEntity.fromSnapshot(doc)))
.toList();
});
Stream stream2 = todoCollection
.where("contributors", arrayContains: userId)
.snapshots()
.map((snapshot) {
return snapshot.documents
.map((doc) => Todo.fromEntity(TodoEntity.fromSnapshot(doc)))
.toList();
});
return StreamZip(([stream1, stream2])).asBroadcastStream();
}
And other hand, I have the following code that will perform the update of view with the Bloc pattern.
Stream<TodosState> _mapLoadTodosToState(LoadTodos event) async* {
_todosSubscription?.cancel();
var res = await _todosRepository.todos(event.userId);
_todosSubscription = res.listen(
(todos) {
dispatch(
TodosUpdated(todos));
},
);
}
I have the following error.
flutter: Instance of '_AsBroadcastStream<List<List<Todo>>>'
[VERBOSE-2:ui_dart_state.cc(148)] Unhandled Exception: type 'List<List<Todo>>' is not a subtype of type 'List<Todo>'
I tried to look for more infos with the debugger and it turned out that my StreamZip source contains the 2 stream separately.
For the moment I can get one stream at a time.
I don't know how to proceed in order to get the 2 streams and display them.

You're doing a map of a map, which returns a List of another List.
You should zip the QuerySnapshot streams and do the mapping after creating the subscription, and then you can create a new Stream<List<Todo>> from it.
///private method to zip QuerySnapshot streams
Stream<List<QuerySnapshot>> _combineStreams() async {
Stream stream1 = todoCollection
.where("owners", arrayContains: userId)
.snapshots()
});
Stream stream2 = todoCollection
.where("contributors", arrayContains: userId)
.snapshots()
});
return StreamZip(([stream1, stream2])).asBroadcastStream();
}
///exposed method to be consumed by repository
Stream<List<Todo>> todosStream() {
var controller = StreamController<List<Todo>>();
_combineStreams().listen((snapshots) {
List<DocumentSnapshot> documents;
snapshots.forEach((snapshot) {
documents.addAll(snapshot.documents);
});
final todos = documents.map((document) {
return Todo.fromEntity(TodoEntity.fromSnapshot(doc));
}).toList();
controller.add(todos);
});
return controller.stream;
}

Related

How to await inside a stream while querying data from Firebase firestore

For context I'm using Getx state management for flutter and i need to call list.bindStream(availabilityStream()) on my Rx<List<Availability>> object.
here is my availabilityStream method
static Stream<List<Availability>> availabilityStream() {
return FirebaseFirestore.instance
.collection('availability')
.where('language',
isEqualTo: GetStorageController.instance.language.value)
.snapshots()
.map((QuerySnapshot query) {
List<Availability> results = [];
for (var availablity in query.docs) {
availablity["cluster"].get().then((DocumentSnapshot document) {
if (document.exists) {
print("Just reached here!");
//! Ignore doc if cluster link is broken
final model = Availability.fromDocumentSnapshot(
availabilityData: availablity, clusterData: document);
results.add(model);
}
});
}
print("result returned");
return results;
});
}
the cluster field on my availability collection is a reference field to another collection. The problem here is i need to await the .get() call to my firestore or the function returns before the data gets returned. I can't await inside the map function or the return type of Stream<List> changes. so how can i await my function call here?
using the advice i got from the comments I've used Stream.asyncMap to wait for all my network call futures to complete.
Here is my updated Repository
class AvailabilityRepository {
static Future<Availability> getAvailabilityAndCluster(
QueryDocumentSnapshot availability) async {
return await availability["cluster"]
.get()
.then((DocumentSnapshot document) {
if (document.exists) {
//! Ignore doc if cluster link is broken
final model = Availability.fromDocumentSnapshot(
availabilityData: availability, clusterData: document);
return model;
}
});
}
static Stream<List<Availability>> availabilityStream() {
return FirebaseFirestore.instance
.collection('availability')
.where('language',
isEqualTo: GetStorageController.instance.language.value)
.snapshots()
.asyncMap((snapshot) => Future.wait(
snapshot.docs.map((e) => getAvailabilityAndCluster(e))));
}
}
How i think this works is that the normal .map function returns multiple promises form the getAvailabilityAndCluster() method then all of the processes that execute asynchronously are all put to Future.wait() which is one big promise that waits all the promises inside it to complete. Then this is passed onto .asyncMap() which waits for the Future.wait() to complete before continuing with its result.

I can't fetch data from two different collection consecutively in Firebase with Flutter

I was trying to fetch from two different collection but I got a weird situation. First, I want to fetch a userID from posts collection. Then with that userID, I want to fetch data from users collection.
So, when I fetch from only the posts collection, print command works perfectly fine and prints the userID.
But when I add the users fetch statement that I showed in the code below it doesn't fetch it and shows an empty string (''), and users collection sends an error because I couldn't search the userID. What am I missing here?
class _ProductDetail extends State<ProductDetail> {
String getTitle = '';
String getLocation = '';
String getPrice = '';
String getImage = '';
String getUniversity = '';
String getProfileImage = '';
String getUserName = '';
String getSellerUserID = '';
#override
Widget build(BuildContext context) {
FirebaseFirestore.instance
.collection('posts')
.doc(widget.postID)
.get()
.then((incomingData) {
setState(() {
getTitle = incomingData.data()!['title'];
getPrice = incomingData.data()!['price'];
getImage = incomingData.data()!['postImage'];
});
});
FirebaseFirestore.instance
.collection('posts')
.doc(widget.postID)
.get()
.then((incomingData) {
setState(() {
getSellerUserID = incomingData.data()!['userID'];
});
});
print(getSellerUserID); //statement that will print the userID
//////////////////////IF I DELETE THIS SECTION, IT PRINTS THE USER ID//////////////////
FirebaseFirestore.instance
.collection('users')
.doc(getSellerUserID)
.get()
.then((incomingData) {
setState(() {
getUserName = incomingData.data()!['username'];
getProfileImage = incomingData.data()!['profileImage'];
getUniversity = incomingData.data()!['university'];
getLocation = incomingData.data()!['location'];
});
});
///////////////////////////////////////////////////////////////////////////////////////////////
return Scaffold(
....... rest of the code
Since data is loaded from Firestore asynchronously, the code inside your then blocks is called (way) later then the line after the call to get().
To see this most easily, add some logging like this:
print("Before calling Firestore")
FirebaseFirestore.instance
.collection('posts')
.doc(widget.postID)
.get()
.then((incomingData) {
print("Got data")
});
print("After calling Firestore")
If you run this code, it'll print:
Before calling Firestore
After calling Firestore
Got data
This is probably not the order you expected, but does explain why your next load from the database doesn't work: the getSellerUserID = incomingData.data()!['userID'] line hasn't been run yet by that time.
For this reason: any code that needs the data from Firestore, needs to be inside the then (or onSnapshot) handler, be called from there, or be otherwise synchronized.
So the simplest fix is to move the next database call into the `then:
FirebaseFirestore.instance
.collection('posts')
.doc(widget.postID)
.get()
.then((incomingData) {
var sellerUserID = incomingData.data()!['userID'];
setState(() {
getSellerUserID = sellerUserID;
});
print(sellerUserID);
FirebaseFirestore.instance
.collection('users')
.doc(sellerUserID)
.get()
.then((incomingData) {
setState(() {
getUserName = incomingData.data()!['username'];
getProfileImage = incomingData.data()!['profileImage'];
getUniversity = incomingData.data()!['university'];
getLocation = incomingData.data()!['location'];
});
});
});

I can't get data from firestore "Instance of 'Future<dynamic>'"

I have this method the get all my documents from a given collection:
getData() async {
await databaseReference
.collection("app").doc('usr').collection(_id).
.get()
.then((querySnapshot) {
querySnapshot.docs.forEach((result) {
return result.data();
});
})
}
What I want to get is all the documents from this collection, and not only the last. With the code above I get this when calling getData()
Instance of 'Future < dynamic>'
What I want to get:
[{name: Victor, age: 18}, {name: Tommy, age: 40}]
How can I reach it?
UPDATE
If I run the code below...:
await databaseReference
.collection("app").doc('usr').collection(_id)
.get()
.then((QuerySnapshot snapshot) {
snapshot.docs.forEach((f) => print(f.data()));
})
In the console it prints all the documents but separately (First prints one, after another):
I/flutter (16316): {name: Victor, age: 18}
I/flutter (16316): {name: Tommy, age: 40}
UPDATE 2
If I write what #Sahil Chadha and #kovalyovi suggest, and just print the list ... :
var items = List<dynamic>();
... my code....
snapshot.docs.forEach((f) => items.add(f.data()));
return items;
//returns exactly what I want
... It returns exactly what I want, but if I write return items and in the calling do var a = getData();, The A value is Instance of future. How can I have the result expected?
UPDATE 3
I forgot the await before getData(). Now it's working:
var a = await getData();
print(a); //my expected result
getData() async {
var items = List<dynamic>();
await databaseReference
.collection("app").doc('usr').collection(_id)
.get()
.then((QuerySnapshot snapshot) {
snapshot.docs.forEach((f) => items.add(f.data()));
})
return items
}
To be able to store the data you receive in a form of a list, you will need to initialize a list at the beginning of the method and then populate that list where you forEach the response. As you mentioned in the comments, I am posting an answer here for you:
getData() async {
// initialize your list here
var items = List<dynamic>();
await databaseReference.collection("app").doc('usr').collection(_id).get().then((QuerySnapshot snapshot) {
snapshot.docs.forEach(
// add data to your list
(f) => items.add(f.data()),
);
});
return items;
}
Like there is no problem with the code. It will be healthier if you just model the data you get. You just did not mention that you were async where you were calling. Since it is future data, there must be a waiting event.
Future<List<User>> getData() async {
await databaseReference
.collection("app").doc('usr').collection(_id).
.get()
.then((querySnapshot) {
querySnapshot.docs.map((f) =>
User.fromJson(f)).toList());
})
}
You should try two of them.
List<User> loaddata=await getData();
--OR------
var loadData;
getData().then((result){
List<User>=result;
});
and you create the user model. It will be more revealing if you do it this way.
import 'dart:convert';
User userFromJson(String str) => User.fromJson(json.decode(str));
String userToJson(User data) => json.encode(data.toJson());
class User {
User({
this.name,
this.age,
});
String name;
int age;
factory User.fromJson(Map<String, dynamic> json) => User(
name: json["name"],
age: json["age"],
);
Map<String, dynamic> toJson() => {
"name": name,
"age": age,
};
}
If there is a problem in the code, you are calling the getData method in the build widget. So you have to call it from initstate.

Stream and Future in flutter

I am using Firebase as backend. Each user has some items and these items cannot be seen by other users. User items are stored in a sub-collection. This is how the structure looks like:
User collection -> User id as document id -> in each document, a sub-collection of items -> item as document.
The app needs to get user id from Firestore and then it can show items of the user.
#override
Stream<List<Item>> items() {
final currentUserId = userRepo.getUserUid();
return Firestore.instance.collection('users')
.document(currentUserId) //error here
.collection("items").snapshots().map((snapshot) {
return snapshot.documents
.map((doc) => Item.fromEntity(ItemEntity.fromSnapshot(doc)))
.toList();
});
}
Future<String> getUserUid() async {
return (await _firebaseAuth.currentUser()).uid;
}
currentUser throws the following error:
The argument type 'Future<String>' can't be assigned to the parameter type 'String'.
I understand that the parameter expects a String and I can't assign a Future but I don't know how use future with stream and resolve the issue. If I replace currentUserId variable with a String like "36o1avWh8cLAn" (the actual user id) it does work.
Any help would be appreciated.
Update:
thanks to Viren V Varasadiya the problem is solved.
#override
Stream<List<Item>> items() async*{
final currentUserId = userRepo.getUserUid();
yield* Firestore.instance.collection('users')
.document(currentUserId) //error here
.collection("items").snapshots().map((snapshot) {
return snapshot.documents
.map((doc) => Item.fromEntity(ItemEntity.fromSnapshot(doc)))
.toList();
});
}
You can use async* annotation to use await in function which return stream.
Stream<List<Item>> items() async*{
final currentUserId = await userRepo.getUserUid();
yield Firestore.instance.collection('users')
.document(currentUserId) //error here
.collection("items").snapshots().map((snapshot) {
return snapshot.documents
.map((doc) => Item.fromEntity(ItemEntity.fromSnapshot(doc)))
.toList();
});
}

Firestore merge 2 queries with Flutter

I am trying to make 2 queries to Firestore and merge the results into one in order to simulate a Firestore OR query.
I segmented my code according to the bloc pattern.
///private method to zip QuerySnapshot streams
Stream<List<QuerySnapshot>> _combineStreams(String userId) {
var stream1 = todosCollection
.where("owners", arrayContains: userId)
.snapshots();
var stream2 = todosCollection
.where("contributors", arrayContains: userId)
.snapshots();
return StreamZip(([stream1, stream2])).asBroadcastStream();
}
///exposed method to be consumed by repository
Stream<List<Todo>> todos(String userId) {
var controller = StreamController<List<Todo>>();
_combineStreams(userId).listen((snapshots) {
List<DocumentSnapshot> documents = List<DocumentSnapshot>();
snapshots.forEach((snapshot) {
documents.addAll(snapshot.documents);
});
final todos = documents.map((doc) {
return Todo.fromEntity(TodoEntity.fromSnapshot(doc));
}).toList();
controller.add(todos);
});
return controller.stream;
}
In my bloc I have the following code that should update my view accordingly my database state but it's not working. The database insertion work but the view doesn't refresh and I don't know why.
_gardensSubscription?.cancel();
_gardensSubscription = _dataRepository.gardens(event.userId).listen(
(gardens) {
dispatch(
GardensUpdated(gardens),
);
},
);
I am not very confortable with Stream and particularly with StreamController process. Is it possible to do this task more easily?
Time to use the great powers of RxDart: https://pub.dev/packages/rxdart
You can't do all types of streams transformations with this lib.
For example, you can use the merge operators to achieve exactly what you want

Resources