Stream with for loop not returning every result - Flutter - firebase

I'm trying to use a Stream with a loop inside a StreamBuilder, but I'm only getting the first result.
I'm using this structure because I need to loop through an array of Items and get only the ones that contain that certain document in Firebase. Could someone please help me figure this out?
This is my Stream with the nested loop:
Stream getProductModels(list) {
var stream;
for(var i = 0; i < list.length; i++) {
stream = databaseReference
.collection('XXXX')
.where('XXXX', isEqualTo: list[i])
.snapshots();
}
return stream;
}

Actually you need to merge several streams into one stream.
There is a Flutter SDK for this.
https://pub.dev/packages/rxdart
You can use MergeStream or ZipStream.
Hope this is helpful for you.

Related

Get data from a map nested in a array in Firestore and Flutter

I have an app where I want to check if there is a contact with a certain uid.
The problem:
The uid I want to check is nested in a map that is in an array.
The Firestore document looks like this:
I thought so far something like this:
var data = await FirebaseFirestore.instance
.collection("user_contacts")
.doc(FirebaseAuth.instance.currentUser.uid)
.get();
if(data.data()["contacts"].contains({"uid": myUid, "date": myDate})){
...
}
But I don't have the date. Is there any other way?
Thanks for help!
I would personally recommend that you try and stay away from arrays within your documents especially if they are going to grow to hundreds of names long. If not careful you could be pulling in unnecessary data on every call.
If you were to change your implementation I would recommend creating a subcollection.
With your current implementation you could use this function:
const exists= (arr, val) => {
for(let i=0; i<arr.length; i++){
return arr[i]['uid'] == val;
};
}
where arr is data.data()["contacts"] and val is your desired uid

Flutter & Firebase Get more than 10 Firebase Documents into a Stream<List<Map>>

With Flutter and Firestore, I am trying to get more than 10 documents into a Stream<List>. I can do this with a .where clause on a collection mapping the QuerySnapshot. However, the 10 limit is a killer.
I'm using the provider package in my app. So, in building a stream in Flutter with a StreamProvider, I can return a
Stream<List<Map from the entire collection. too expensive. 200 plus docs on these collections and too many users. Need to get more efficient.
Stream<List<Map using a .where from a Collection that returns a Stream List 10 max on the list...doesn't cut the mustard.
Stream<Map from a Document, that returns 1 stream of 1 document.
I need something in between 1 and 2.
I have a Collection with up to 500 Documents, and the user will choose any possible combination of those 500 to view. The user assembles class rosters to view their lists of users.
So I'm looking for a way to get individual streams of, say 30 documents, and then compile them into a List: But I need this List<Stream<Map to be a Stream itself so each individual doc is live, and I can also filter and sort this list of Streams. I'm using the Provider Package, and if possible would like to stay consistent with that. Here's where I am currently stuck:
So, my current effort:
Future<Stream<List<AttendeeData>>> getStreams() async {
List<Stream<AttendeeData>> getStreamsOutput = [];
for (var i = 0; i < teacherRosterList.length; i++) {
Stream thisStream = await returnTeacherRosterListStream(facility, teacherRosterList[i]);
getStreamsOutput.add(thisStream);
}
return StreamZip(getStreamsOutput).asBroadcastStream();
}
Feels like I'm cheating below: I get an await error if I put the snapshot directly in Stream thisStream above as Stream is not a future if I await, and if I don't await, it moves too fast and gets a null error.
Future<Stream<AttendeeData>> returnTeacherRosterListStream(String thisFacility, String thisID) async {
return facilityList.doc(thisFacility).collection('attendance').doc(thisID).snapshots().map(_teacherRosterListFromSnapshot);
}
}
Example of how I'm mapping in _teacherRosterListFromSnapshot (not having any problem here):
AttendeeData _teacherRosterListFromSnapshot(DocumentSnapshot doc) {
// return snapshot.docs.map((doc) {
return AttendeeData(
id: doc.data()['id'] ?? '',
authorCreatedUID: doc.data()['authorCreatedUID'] ?? '',
);
}
My StreamProvider Logic and the error:
return MultiProvider(
providers: [
StreamProvider<List<AttendeeData>>.value(
value: DatabaseService(
teacherRosterList: programList,
facility: user.claimsFacility,
).getStreams()),
]
Error: The argument type 'Future<Stream<List>>' can't be assigned to the parameter type 'Stream<List>'.
AttendeeData is my Map Class name.
So, the summary of questions:
Can I even do this? I'm basically Streaming a List of Streams of Maps....is this a thing?
If I can, how do I do it?
a. I can't get this into the StreamProvider because getStreams is a Future...how can I overcome this?
I can get the data in using another method from StreamProvider, but it's not behaving like a Stream and the state isn't updating. i'm hoping to just get this into Provider, as I'm comfortable there, and I can manage state very easily that way. However, beggars can't be choosers.
Solved this myself, and since there is a dearth of good start to finish answers, I submit my example for the poor souls who come after me trying to learn these things on their own. I'm a beginner, so this was a slog:
Objective:
You have any number of docs in a collection and you want to submit a list of any number of docs by their doc number and return a single stream of a list of those mapped documents. You want more than 10 (firestore limit on .where query), less than all the docs...so somewhere between a QuerySnapshot and a DocumentSnapshot.
Solution: We're going to get a list of QuerySnapshots, we're going to combine them and map them and spit them out as a single stream. So we're getting 10each in chunks (the max) and then some odd number left over. I plug mine into a Provider so I can get it whenever and wherever I want.
So from my provider I call this as the Stream value:
Stream<List<AttendeeData>> filteredRosterList() {
var chunks = [];
for (var i = 0; i < teacherRosterList.length; i += 10) {
chunks.add(teacherRosterList.sublist(i, i + 10 > teacherRosterList.length ? teacherRosterList.length : i + 10));
} //break a list of whatever size into chunks of 10.
List<Stream<QuerySnapshot>> combineList = [];
for (var i = 0; i < chunks.length; i++) {
combineList.add(*[point to your collection]*.where('id', whereIn: chunks[i]).snapshots());
} //get a list of the streams, which will have 10 each.
CombineLatestStream<QuerySnapshot, List<QuerySnapshot>> mergedQuerySnapshot = CombineLatestStream.list(combineList);
//now we combine all the streams....but it'll be a list of QuerySnapshots.
//and you'll want to look closely at the map, as it iterates, consolidates and returns as a single stream of List<AttendeeData>
return mergedQuerySnapshot.map(rosterListFromTeacherListDocumentSnapshot);
}
Here's a look at how I mapped it for your reference (took out all the fields for brevity):
List<AttendeeData> rosterListFromTeacherListDocumentSnapshot(List<QuerySnapshot> snapshot) {
List<AttendeeData> listToReturn = [];
snapshot.forEach((element) {
listToReturn.addAll(element.docs.map((doc) {
return AttendeeData(
id: doc.data()['id'] ?? '',
authorCreatedUID: doc.data()['authorCreatedUID'] ?? '',
);
}).toList());
});
return listToReturn;
}

Flutter using a stream within another stream

I have a flutter project that uses firebase as a database. I am trying to get a list of document names via a stream, break it into sets of 10 to workaround the 10 comparison maximum for the .where() function in firebase then make batch requests using .where() and merge them.
The problems that I am aware of are:
friends.length cannot be used as a number in a loop because it is of type future
The firestore query return cannot be added to the stream list for reasons I do not understand ("The argument type 'Query' can't be assigned to the parameter type 'Stream'.")
.where() is expecting 1 positional argument but I am giving it 3, though this is what the firestore documentation says I should do
StreamGroup is underfined
Stream<List<Memo>> get memos {
// create list of streams
List<Stream> streams = [];
// Get list of friend userids
var friends = Firestore.instance.collection("users").document(userid).collection("friends").snapshots().map(_snapshotToStringList);
// split friend userid list into chunks of 10
for (var i = 0; i < friends.length; i += 10) {
// query database for each chunk of 10 and add to stream list
streams.add(Firestore.instance.collection("memos").where("userid", "in", friends.sublist(i, i+10)));
}
// merge and return stream list
return StreamGroup.merge(streams);
}
Any and all advice is appreciated
Use this code:
Stream<List<Memo>> get memos {
List<Stream> streams = [];
Firestore.instance
.collection("users")
.document(userid)
.collection("friends")
.snapshots()
.map(_snapshotToStringList)
.listen((friends){
for (var i = 0; i < friends.length; i += 10) {
// query database for each chunk of 10 and add to stream list
streams.add(Firestore.instance
.collection("memos")
.where("userid", isEqualTo: friends.sublist(i, i+10)));
}
});
return StreamGroup.merge(streams);
}

How to get data from firebase query based on value from another firebase query in FutureBuilder in Flutter?

I am new to flutter and I am sure there is a simple way of doing this. Let me first give you a background. I have 2 tables(collections). The first one store a mapping. Therefore it returns a key based on an id which will be used to query the second table and retrieve the data from firebase.
I have written 2 data models and 2 functions which return Future<> data. They are as follows-
Future<SpecificDevice> getSpecificDevice(String deviceId) {
Future<SpecificDevice> obj =_database.reference().child("deviceMapping").orderByChild("deviceId").equalTo(deviceId).once().then((snapshot) {
SpecificDevice specificDevice = new SpecificDevice(deviceId, "XXXX", new List<String> ());
if(snapshot.value.isNotEmpty){
print(snapshot.value);
snapshot.value.forEach((key,values) {
if(values["deviceId"] == deviceId) {
specificDevice.deviceKey = values["deviceDesc"];
specificDevice.vendorList = List.from(values["vendorList"]);
}
});
}
return specificDevice;
});
return obj;
}
This function gets the mapping deviceId -> deviceKey.
This is the key of record stored in another table. Following is the function for it.
Future<Device> getDeviceDescription(String deviceKey) {
Future<Device> device = _database.reference().child("deviceDescription").once().then((snapshot) {
Device deviceObj = new Device("YYYY", "YYYY", "YYY", "YYYY", "YYYY");
if(snapshot.value.isNotEmpty){
print(snapshot.value);
//Future<SpecificDevice> obj = getSpecificDevice(deviceId);
//obj.then((value) {
snapshot.value.forEach((key,values) {
if(key == deviceKey) { // compare with value.deviceKey instead
print(values["deviceDescription"]); // I get the correct data here.
deviceObj.manual = values["deviceManual"];
deviceObj.deviceType = values["deviceType"];
deviceObj.description = values["deviceDescription"];
deviceObj.brand = values["deviceBrand"];
deviceObj.picture = values["devicePicture"];
}
// });
});
}
return deviceObj;
});
return device;
}
Now both of these functions work. I want to make it work one after the other. In the above function, if I uncomment the lines of code, the data is retrieved properly in the inner function but it returns initial default values set because the values get returned before setting the obj of SpecificDevice.
Here is where I am getting the error. I am calling the second function in FutureBuilder<> code with the above lines uncommented and taking input param as deviceId.
return FutureBuilder<Device>(
future: getDeviceDescription(deviceId),
builder:(BuildContext context,AsyncSnapshot snapshot){... // using snapshot.data in its child.
Here in snapshot.data. would give me YYYY. But it should get me the value from the database.
I am stuck with this for a while. Any help in fixing this? or if what I am trying to do is clear then please suggest me a better way to approach this. Thanks in advance!
The answer is rather simple:
first and foremost - you forgot to use async / await keywords, which will guarantee synchronous data retrieval from the database. Always use them, if you are connecting to any network service
to make one command work after another - use .then((value) {}). It will get data from the first function (which you pass using return) and use it in the second function.
Solved the problem by changing the calling function to -
return FutureBuilder<Device>(
future: getSpecificDevice(deviceId).then((value){
return getDeviceDescription(value.deviceKey);
}),
builder:(BuildContext context,AsyncSnapshot snapshot){

Flutter Multiple Firestore Queries

I am trying to make multiple queries to Firestore and merge the results into one stream like here. I tried using StreamGroup.merge() but it's only returning the results of one stream. I noticed that it does actually get data for all the streams but only returns one when everything completes. Here is what I did:
Stream getStream(){
List<Stream> streams = [];
streams.add(Firestore.instance.collection(Constants.REQUESTS_NODE).
where("municipality",isEqualTo: "City of Johannesburg Metropolitan").
where("service_id",isEqualTo: 2).
snapshots());
streams.add(Firestore.instance.collection(Constants.REQUESTS_NODE).
where("municipality",isEqualTo: "Lesedi Local").where("service_id",isEqualTo: 2).
snapshots());
return StreamGroup.merge(streams);
}
What am I missing and doing wrong? The reason I'm doing this is to compensate for Firestore's lack of OR operator.
I found this online, hope it helps:
`import 'package:async/async.dart';
Stream<DocumentSnapshot> stream1 = firestore.document('/path1').snapshots();
Stream<DocumentSnapshot> stream2 = firestore.document('/path2').snapshots();
StreamZip bothStreams = StreamZip([stream1, stream2]);
// To use stream
bothStreams.listen((snaps) {
DocumentSnapshot snapshot1 = snaps[0];
DocumentSnapshot snapshot2 = snaps[1];
// ...
});`
You can use whereIn condition, it's a List<dynamic>, that's work with me!
For example:
Firestore db = Firestore.instance;
db.collection
.where("status", whereIn: ["available", "unavailable", "busy"])
.getDocuments();

Resources