I'm obviously misunderstanding how StreamGroup.merge works or how stream works or both!. I have two streams that are querying the same field in firestore. And I'm using merge to make one stream called mergedStream. This is used in a streamBuilder to serve up to the app.
Here's my code:
Stream<List<Order>> stream1({Order order}) {
return _service.collectionStream(
path: APIPath.orders(),
queryBuilder: (query) =>
query.where('orderStatus', isEqualTo: 'Pending'),
builder: (documentSnapshot) => Order.fromFirebase(documentSnapshot));
}
Stream<List<Order>> stream2({Order order}) {
return _service.collectionStream(
path: APIPath.orders(),
queryBuilder: (query) =>
query.where('orderStatus', isEqualTo: 'Preparing'),
builder: (documentSnapshot) => Order.fromFirebase(documentSnapshot));
}
and the merged stream I use for my stream builder:
Stream<List<Order>> mergedStream() {
final s1 = preparingStream();
final s2 = pendingStream();
return StreamGroup.merge([s2, s1]);
}
Switching the order [s2, s1] changes the stream shown. And as the page loads I momentarily see the other stream but it disappears and the other stream appears..How can I merge the streams...?
EDIT: I'm adding the flutter tag because reading around it might be a problem with StreamBuilder
EDIT: This has helped a bit:(link in comments, And was just a proof of concept that the streams merged) But the stream searchResult() doesn't update when there's a state changes in one of he streams...You have to refresh the browser. (Flutter web)
Stream<List<Order>> searchResult() {
List<Stream<List<Order>>> streamList = [stream1(), stream2()];
var x = Rx.merge(streamList).scan<List<Order>>((acc, curr,i) {
return acc ?? <Order>[]
..addAll(curr);
});
//this test shows that the stream gets contents from both query streams
// x.map((convert){ convert.documents.forEach((f){print(f.data["name"]);});}).listen(print);
return x;
}
StreamGroup.merge merge the streams into an unified streams, but it doesn't merge the content of each one, so what you see in fact is the content of stream1 or stream2 coming one after the other.
I would recommend using StreamZip which combines the contents of the 2 streams and then emmits it as a new value (The first list contains the first value emitted by each stream, the second contains the second value, and so on).
Stream<List<List<Order>>> mergedStream() {
final s1 = preparingStream();
final s2 = pendingStream();
return StreamZip([s2, s1]);
}
//This will give you a List with all the emmited values of each stream, in this case a List of List<Order>
If you want to use the package Rx I would recommend ZipStream or CombineLatestStream
import 'package:rxdart/rxdart.dart';
Stream<List<Order>> get combineList{
final s1 = preparingStream();
final s2 = pendingStream();
return ZipStream.zip2<<List<Order>>, <List<Order>>, <List<Order>>>(
s1, s2, (firstOrder, secondOrder) => <Order>[...firstOrder, ...secondOrder]); //here you combine both in a list
}
The difference between Zip and Combine is that Zip waits for all the inner streams to emit before doing the operation (first emmited value of s1 and first of s2, and so on) whereas combine doesn't care about the order, if you emit one of s1 and 3 of s2 then it will do [s1.1, s2.1] [s1.1, s2.2] ...
Related
I had to do some work around combining streams where the returned streams would either be stream list of query snapshot, or stream lists of document snapshot. In any case I had to combine the streams and output the result in the UI. I chose not to use rxdart and i wasn't creating any special classes for the particular objects I wanted.
I did run into some major issues with async generator methods.
question 1:
The async generator will only await for one function and ignore any other awaits inside the generator function e.g.
when creating a function e.g.
Stream<List> testFunc3(Stream<List> stream, Stream<List> stream2, List<dynamic> anArray,
List<dynamic> anArray2) async* {
print('testfunc 3');
List streamList = [];
List streamList2 = [];
await for (List<dynamic> a in stream) {
streamList.clear();
a.forEach((element) {
if (anArray.contains(element['name'])) {
if (anArray2.contains(element['name'])) {
print('test 1');
if (!streamList.contains(element)) streamList.add(element);
}
}
});
yield streamList;
}
await for (List<dynamic> a in stream2) {
streamList2.clear();
a.forEach((element) {
if (anArray.contains(element['name'])) {
if (anArray2.contains(element['name'])) {
print('test 2');
}
}
});
yield streamList2;
}
}
is there a way to get around this behaviour?
question 2:
If I declare multiple variables that are functions which return streams, only the methods that are used in say StreamGroup.merge() are executed. streams not included in the merge never run.
e.g. z never returns a result because testfunc3 is never executed. unless i add z to the StreamGroup.merge().
keep in mind this isn't the whole function, just the relevant sections and i'm aware this method needs to yield a result
Stream<List> getStreamPlaces() async* {
Stream<List> x = this.testFunc2(stream, anArray);
Stream<List> y = this.testFunc2(textStream, anArray2);
Stream<List> z = this.testFunc3(radius);
Stream<List> g = StreamGroup.merge([x, y]);
// yields something
}
because of these issues i had to do some long winded approaches to filtering and getting the stream output I wanted. Is this a limitation or bug in dart or am i missing some fundamental understanding? Is there a keyword I could have used to force the execution of these methods?
any help would be appreciated.
for question 2. i do remember codewithAndrea briefly mentioned that an optimization of dart or flutter prevents streams from being executed unless they are used in the code but I couldn't find it when I went searching through his vids.
I have 2 Firestore streams where the 2nd one depends on the 1st one. I want to emit new models that depend on data from both streams.
In pseudocode you can think of it in a following way:
// 1st stream emits some device data
final streamA = client.device(id: 0); // Stream<DeviceData>
// 2nd stream emits some metadata for the above device type
final streamB = client.deviceMetadata(type: device.type); // Stream<DeviceMetadata>
// and finally I want to receive stream of combined device (device data + metadata)
final combinedObject = Device.from(deviceData, deviceMetadata);
However, I cannot find an effective way of mapping a stream onto another stream while accessing the value from the source stream. For instance with .map() I would like to write:
return client.device('deviceId')
.map((device) => client.deviceMetadata(type: device.type))
.map((deviceMetadata) => Device.from(???, deviceMetadata));
The problem is of course that I don't know how to access device in the second map().
What stream transformation or rxdart extension I could use to achieve that?
Ok, after some tweaking I think I've found a solution
// get the initial stream
final deviceStream = client.device(id: id);
// create new stream depending on the first one
final deviceTypeStream = deviceStream.flatMap( // make sure to flatten the stream so that it's not Stream<Stream<..>>
(device) => client.deviceMetadata(type: device.type),
);
// use withLatestFrom to capture the latest value from the deviceTypeStream
// and create desired object out of that
return deviceTypeStream.withLatestFrom<DeviceData, Device>(
deviceStream,
(deviceType, device) => Device(device, deviceType),
);
You can use the rxdart library to combine multiple streams into one stream https://pub.dev/documentation/rxdart/latest/rx/CombineLatestStream-class.html
I have two streams:
Stream<List<Order>> stream1 = pendingStream();
Stream<List<Order>> stream2 = preparingStream();
I'm trying to use StreamZip from the package:async/async.dart package to merge the streams like so...
Stream<List<Order>> getData() {
Stream<List<Order>> stream1 = pendingStream();
Stream<List<Order>> stream2 = preparingStream();
return StreamZip([stream1, stream2]);
}
However it won't compile. Saying:
The element type 'Stream<List<Order>>' can't be assigned to the list type 'Stream<Order>'.
From what I understand StreamZip should accept the two streams? What am I dong wrong?
You are creating a StreamZip<T> which will emit a List<T> of each event of its merged streams as you can refer in the documentation.
Each of your merged streams emit a List<Order> type, so that means that you will create a merged stream that will emit a List of List.
Basically, you only need to change your return type from Stream<List<Order>> to Stream<List<List<Order>>>.
Please help me understand, why this code not working!
I try to get data from a Stream (Firestore), and take this data to a list. I want to wait until the list is ready, and with this list do something. But .then or .whenComplete fires before the list is ready...
This is the function to make the list and return it:
Future<List<EventDistance>> getEventsDistanceList(String eventId) async{
Stream<FS.QuerySnapshot> qs = EventDistanceDataRepository().getStreamByEventId(eventId: eventId);
List<EventDistance> dList = [];
EventDistance eventDistance;
qs.forEach((document) {
document.forEach((docs) {
eventDistance = eventDistanceFromJson(docs.data());
dList.add(eventDistance);
print(eventDistance.Name); //(3.) only for testing, to see if docs is not empty
}
);
});
print('return'); //(1.) only for testing, to see when return is fired
return dList;
}
(return also fires before)
i use this code so:
Future<List<EventDistance>> dList = getEventsDistanceList(filteredList[index].id );
dList.then((value) {
print('value: $value'); //(2.) only for testing,to see if the returned list is empty or not (empty :-( )
doSomething;
});
When i run, i recive first 'return' (1.), then 'value: null' (2.) (and an empty list) and then the elements of the list (Name1, Name2 ...) (3.).
What do i wrong? How to wait to receive the list first?
Thanks for the answeres!
To become more confident with async operations read the perfect
article by Didier Boelens
Let check what is going on in your code
Your getEventsDistanceList() routine is pure synchronous - all of it's content runs synchronously step by step
synchronously subscribe to a Stream in qs.forEach and set callback listener (document) { ... } which will be fired on each stream item somewhere in future
synchronous call print('return') is fired
finally getEventsDistanceList() returns
you listen to this Future returned from getEventsDistanceList() until it complete and then then() is fired with call to print('value: $value')
first stream item is received and callback fired with print(eventDistance.Name)
5th step will repeat with new items until stream completes or ended with error (see Stream.forEach implementation)
I supposed you need only first Stream item (if not, do not hesistate reach me in comments)
If so rewrite your code
EventDistanceDataRepository()
.getStreamByEventId(eventId: eventId)
.first
.then((document) => document.map((docs) => eventDistanceFromJson(docs.data())).toList())
.then((value) { doSomething;});
I prefer more readable await notation
final FS.QuerySnapshot document = await EventDistanceDataRepository()
.getStreamByEventId(eventId: eventId)
.first;
final List<EventDistance> listOfEvents = document.docs.map((e) => eventDistanceFromJson(e.data())).toList();
doSomething with this list
You need to use await in asynchronous functions. I'm guessing
Stream<FS.QuerySnapshot> qs =
EventDistanceDataRepository().getStreamByEventId(eventId: eventId);
Should be
Stream<FS.QuerySnapshot> qs = await
EventDistanceDataRepository().getStreamByEventId(eventId: eventId);
Where ever the operation that takes a long time happens gets the await keyword.
Try the code labs to get better with async await
works fine! the final code is:
final List<EventDistance> listOfEvents = document.docs.map((e) => eventDistanceFromJson(e.data())).toList();
I'm querying Firestore and getting a Stream back as a Stream of QuerySnapshots. I need to map the included Documents in the stream to a List of objects.
The code below doesn't work (obviously)...maybe I'm just looking at this entirely wrong.
List<UserTask> getUserTaskList() {
List<UserTask> list;
Stream<QuerySnapshot> stream =
Firestore.instance.collection('userTasks').snapshots();
stream.listen((snapshot) {
snapshot.documents.forEach((doc) {
UserTask userTask = UserTask(
doc.data['id'],
doc.data['Description'],
etc...);
list.add(userTask);
});
});
return list;
}
With the code above, since it doesn't wait for the entire stream (or any of it actually), list is always returned as null. In short, how do I convert my stream to a List?
Note: I'm pretty new to the world of Dart, so go easy on me :)
Thanks!
First of all, think about this: this function has to return very quickly. All functions do, otherwise UI would hang. However, you are expecting the function to return something that comes from the internet. It takes time. The function has to return. There is no way for a function to simply do a network request and return you the result. Welcome to the world of asynchronous programming.
Furthermore, the stream you have is not a stream of DocumentSnapshots (which you can convert to UserTasks), but a stream of QuerySnapshots (which you can convert to List<UserTask>s). Notice the plural there. If you simply want to get all your UserTasks once, you should have a Future instead of a Stream. If you want to repeatedly get all your UserTasks after each change, then using a Stream makes sense.
Since you said you want to get a List<UserTask>, I'm assuming you want to get the collection of UserTasks only once.
Here's what your code becomes in this light:
Future<List<UserTask>> getUserTaskList() async {
QuerySnapshot qShot =
await Firestore.instance.collection('userTasks').getDocuments();
return qShot.documents.map(
(doc) => UserTask(
doc.data['id'],
doc.data['Description'],
etc...)
).toList();
}
main() async {
List<UserTask> tasks = await getUserTaskList();
useTasklist(tasks); // yay, the list is here
}
Now if you really wanted to use a stream, here's how you could do it:
Stream<List<UserTask>> getUserTaskLists() async {
Stream<QuerySnapshot> stream =
Firestore.instance.collection('userTasks').snapshots();
return stream.map(
(qShot) => qShot.documents.map(
(doc) => UserTask(
doc.data['id'],
doc.data['Description'],
etc...)
).toList()
);
}
main() async {
await for (List<UserTask> tasks in getUserTaskLists()) {
useTasklist(tasks); // yay, the NEXT list is here
}
}
Hope it helps.