Flutter merge two firestore streams into a single stream - firebase

I simply want to perform an 'OR' operation and get the both results of two queries into one stream.
Here's my code with a single stream
StreamBuilder(
stream: Firestore.instance
.collection('list')
.where('id', isEqualTo: 'false')
.orderBy('timestamp')
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: CircularProgressIndicator(),
)
],
);
if (snapshot.data.documents.length == 0)
return const Center(
child: Text(
"Not Available",
style: TextStyle(fontSize: 30.0, color: Colors.grey),
),
);
return ListView.builder(
padding: EdgeInsets.all(5.0),
key: Key(randomString(20)),
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index) {
return ListCard(snapshot.data.documents[index]);
},
);
}),
Instead of a single stream now I want to feed two stream to the same stream builder.
I tried StreamGroup but it's not working since Widgets rebuild
StreamGroup.merge([streamOne, streamTwo]).asBroadcastStream();
I tried followed method also
Stream<List<DocumentSnapshot>> searchResult() {
List<Stream<List<DocumentSnapshot>>> streamList = [];
Firestore.instance
.collection('room-list')
.where('id', isEqualTo: 'false')
.snapshots()
.forEach((snap) {
streamList.add(Observable.just(snap.documents));
});
Firestore.instance
.collection('room-list')
.where('id', isEqualTo: 'pending')
.snapshots()
.forEach((snap) {
streamList.add(Observable.just(snap.documents));
});
var x = Observable.merge(streamList)
.scan<List<DocumentSnapshot>>((acc, curr, i) {
return acc ?? <DocumentSnapshot>[]
..addAll(curr);
});
return x;
}
Here I get the error there should be at least a single stream to merge. Its because Observable.merge(streamList) is called before items are added to streamList.
I simply want to get the both results of two queries into one stream.

This should work.
//Change your streams here
Stream<List<QuerySnapshot>> getData() {
Stream stream1 = Firestore.instance.collection('list').where('id', isEqualTo: 'false').orderBy('timestamp').snapshots();
Stream stream2 = Firestore.instance.collection('list').where('id', isEqualTo: 'true').orderBy('timestamp').snapshots();
return StreamZip([stream1, stream2]);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: StreamBuilder(
stream: getData(),
builder: (BuildContext context, AsyncSnapshot<List<QuerySnapshot>> snapshot1) {
List<QuerySnapshot> querySnapshotData = snapshot1.data.toList();
//copy document snapshots from second stream to first so querySnapshotData[0].documents will have all documents from both query snapshots
querySnapshotData[0].documents.addAll(querySnapshotData[1].documents);
if (querySnapshotData[0].documents.isEmpty)
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: CircularProgressIndicator(),
)
],
);
if (querySnapshotData[0].documents.length == 0)
return const Center(
child: Text(
"Not Available",
style: TextStyle(fontSize: 30.0, color: Colors.grey),
),
);
return new ListView(
children: querySnapshotData[0].documents.map((DocumentSnapshot document){
// put your logic here. You will have access to document from both streams as "document" here
return new ListCard(document);
}).toList()
);
}
),
);
}
Hope this helps!!!

I’m not sure why you’re using forEach and Observable.just().
You can just merge two firestore streams directly like:
Observable.merge([stream1, stream2]).pipe(combineStream);
Wherre stream1/2 is just your firestore snapshot.

I used RxDart package to combine two streams as shown below
RxDart - CombineLatestStream
final Stream<DocumentSnapshot> user = Firestore.instance
.collection("users")
.document(firebaseUser.uid)
.snapshots();
final Stream<QuerySnapshot> cards =
Firestore.instance.collection("cards").snapshots();
CombineLatestStream.list([user, cards]).listen((data) {
add(LoadedHomeEvent(
data.elementAt(0),
data.elementAt(1),
));
});

Well I am late, but just gonna put it out there.
You can add whereIn clause in your query like this:
Firestore.instance.collection("collection_name").where("field",whereIn:["false","true"]).snapshots();

I was also trying to combine two streams from firestore (as querying does not support OR) and went about it like this:
import 'package:rxdart/rxdart.dart';
Rx.combineLatest2(
StreamQuerySnapshot1, //a snapshot from firestore
StreamQuerySnapshot2, //another snapshot from firestore
(var stream1, var stream2) {
return [...stream1.docs, ...stream2.docs]; //Concatenated list
}
)
This will emit changes no matter which streams is changing in contrast to other solutions I found which support emits only if both streams have changes.

The best way I found is to use MergeStream from RxDart
Stream<QuerySnapshot> searchResult() {
final falseRoomStream = FirebaseFirestore.instance
.collection('room-list')
.where('id', isEqualTo: 'false')
.snapshots();
final pendingRoomStream = FirebaseFirestore.instance
.collection('room-list')
.where('id', isEqualTo: 'pending')
.snapshots();
return MergeStream([falseRoomStream, pendingRoomStream]);
}

Related

Reading FirebaseFirestore collection items and saving them to list

I am having a trouble reading collection from firebase and saving values in a list.
I basically have a collection called 'brands' where I have car brands like this:
Firebase 'brands' collection screenshot
I need these car brands to be saved as a list like this, to be able to use it in a dropdown menu as items:
<String>[
'ferrari',
'mercedes',
'porsche',
]
I have tried using StreamBuilder (below) but it requires to return a widget and I do not actually need a widget to be returned, so below StreamBuilder is just an experiment "in progress".
Do you have any ideas?
final stream = FirebaseFirestore.instance
.collection('accounts')
.doc('dealers')
.collection(user!.uid)
.doc(dealerName)
.collection('brands')
.snapshots();
StreamBuilder(
stream: stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text('Error in receiving snapshot: ${snapshot.error}');
}
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor,
),
);
}
return ListView.builder(
padding: EdgeInsets.all(8),
reverse: true,
itemCount: snapshot.data.docs!.length,
itemBuilder: (BuildContext context, int index) {
return Text(
snapshot.data.docs[index]['brandName'],
);
},
);
},
);
Once you get the data from firebase, loop through it and add the car brands to your list. Try this:
List<String> myBrands = [];
final dataRef = await FirebaseFirestore.instance
.collection('accounts')
.doc('dealers')
.collection(user!.uid)
.doc(dealerName)
.collection('brands')
.get();
dataRef.docs.forEach((doc) {
myBrands.add(doc.data()['brandName']);
});
You should then be able to use the myBrands list for your dropdown menu.

Merging stream in flutter firetore

I am using two quires to fetch data from the firestore.
Query 1.
_firestore
.collection('chats')
.doc(getCurrentUser().uid)
.collection('chatUsers')
.orderBy('timestamp');
with all the querysnapshot document from query 1. I am fetching last message and document id, and displaying the last message in listTile. With document id i am passing the id to fetch other data from other collection like name photo etc.
Query 2.
Future<DocumentSnapshot> fetchUserData(String uid) async => await _firestore
.collection('users')
.doc(uid).get();
So for this I need to use nested stream builder. First stream builder to fetch all data. Second stream builder to fetch user requested data from all data. what will be the best approach?
This is how i am using query 1 in my widgets for the query 2 I have to implement it inside the ListView.builder which will be the nested stream. please guide me with the best approach to this.
SafeArea(
child: Scaffold(
body: StreamBuilder<QuerySnapshot>(
stream: _fetchUserChatRoom.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
return _tiles(snapshot.data.docs);
} else if (snapshot.hasError) {
return Icon(Icons.error_outline);
} else {
return CircularProgressIndicator();
}
}),
),
);
}
Widget _tiles(List<QueryDocumentSnapshot> docs) {
return ListView.builder(
itemCount: docs.length,
itemBuilder: (BuildContext context, int index) {
var data = ChatModel.fromMap(docs[index].data());
return GestureDetector(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => ChatScreen(uid: docs[index].id))),
child: ListTile(
leading: CircleAvatar(),
title: Text(data.message),
subtitle: Text(data.timestamp.toString()),
trailing: Text('time'),
),
);
});
You can either use async and await in your ListView.builder, however, I imaging this could slowdown you app and cause a lot of firestore calls.
Widget _tiles(List<QueryDocumentSnapshot> docs) {
return ListView.builder(
itemCount: docs.length,
itemBuilder: (BuildContext context, int index) async {
var data = ChatModel.fromMap(docs[index].data());
var userData = await fetchUserData(data[index].uid);
return GestureDetector(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => ChatScreen(uid: docs[index].id))),
child: ListTile(
leading: CircleAvatar(),
title: Text(data.message),
subtitle: Text(data.timestamp.toString()),
trailing: Text('time'),
),
);
});
Other options (which I use) is to use a Provider class with all the contacts. You can fill the Provider when the app initializes with all the users in your firestore. After that you can use each user data anywhere in your app.

Perform stream calculations in app "brain" - An alternative to Stream Builder

I am listening to a number of streams with RxDart. I then use a StreamBuilder to process the data as it comes in and update String and int variables that I then display in my app. Streams and variables are displayed below.
Stream<QuerySnapshot> janFlightsStream(BuildContext context) async* {
final uid = await Provider.of(context).auth.getCurrentUID();
yield* Firestore.instance
.collection("UserAirTravelInputs")
.document(uid)
.collection(formatDate(DateTime.now(), [yyyy, '', '']))
.where("entryMonth", isEqualTo: "Jan")
.snapshots();
}
Stream<QuerySnapshot> febFlightsStream(BuildContext context) async* {
final uid = await Provider.of(context).auth.getCurrentUID();
yield* Firestore.instance
.collection("UserAirTravelInputs")
.document(uid)
.collection(formatDate(DateTime.now(), [yyyy, '', '']))
.where("entryMonth", isEqualTo: "Feb")
.snapshots();
}
double janFlightsKm;
int janNumberOfEntries;
StreamBuilder returns a widget which is fine here. But I do this on each screen in my app which is extremely inefficent. I want to extract this logic (of listening to firebase stream data and making calculations based on that data) from each screen to something like a provider or app "brain" which handles this data, so that I only need tp access the output of these calculations when needed in the different app routes.
StreamBuilder<List>(
stream: CombineLatestStream.list([
janFlightsStream(context),
febFlightsStream(context),
]),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SpinKitDoubleBounce(
color: kDarkBlue,
size: 100,
),
Text("Loading data..."),
],
),
);
}
if (snapshot.data[0].documents.length != 0) {
final janFlights = snapshot.data[0].documents;
double janTotalGCO2e = 0;
janNumberOfEntries = snapshot.data[0].documents.length;
for (var flight in janFlights) {
double janKm = flight.data["flightKm"];
janTotalGCO2e += janKm;
}
janFlightsKm =
double.parse((janTotalGCO2e / 1000).toStringAsFixed(2));
} else {
janFlightsKm = 0;
janNumberOfEntries = 0;
}
if (snapshot.data[1].documents.length != 0) {
//Do something
} // Do something else
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(),
body: Container(
child: Column(
children: [
Text(janNumberOfEntries.toString()),
Text(janFlightsKm.toString()),
],
),
),
);
}),
Is there a way to do this without using this repeated StreamBuilder? I have looked in to StreamProvider but appears that only gives the stream of data, and I would then have to process the data again each time I tap in to the StreamProvider which does not solve the problem.
Simply it is 1. list of streams. 2. process data coming from streams (as code above), 3. store that data, and 4. make it available throughout the app.
Any help would be greatly appreciated!

how to get all record with today's date from firebase

my code displays all records, from collection, how i can get record with today's timestamp, i searched for examples from google, but i don't know where to use "where" condition, in my code.
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('baby').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return Container(
margin: EdgeInsets.only(top: 200),
child:ListView(
children: snapshot.map((data) => listSection(context, data)).toList(),
)
);
}
where(Common.DATE_TIME,
isGreaterThanOrEqualTo: DateTime(date.year, date.month, date.day))
This is working awesome for me...
For now I don't know if there is a way to do that with the firestore API. But this could be a temporal fix
const oneDay = 60 * 60 * 24 * 1000;
List filteredList = List();
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('baby')
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot){
snapshot.forEach((snap){
if((DateTime.now()
.difference(DateTime.parse(snap.data['timestamp']
.toDate().toString())))
.inMilliseconds <= oneDay){
filteredList.add(snap);
}
});
return Container(
margin: EdgeInsets.only(top: 200),
child:ListView(
children: filteredList.map((data) => listSection(context, data)).toList(),
)
);
}
You can use the client library cloud_firestore/cloud_firestore.dart which is is the Dart Client library for firestore.
Using it you can get the data like this:
var today = new DateTime.now();
today = new DateTime(today.year, today.month, today.day);
var data = Firestore.instance.collection('talks')
.where("topic", isGreaterThan: today)
.snapshots()
after this you can use your data to form the widgets as usual
here you can see the where syntax with the client library.

How can I Paginate Firestore Data in ListView.Builder and Still Get Realtime Updates in Flutter?

I can't figure out how to paginate Firestore data without breaking the realtime listener. The data is passed to a StreamBuilder and displayed in a ListView.builder. I'm trying to fetch the next set of data when the user reaches maxScrollExtent.
I understand how to to use startAfter and limit with Firestore to paginate. If I pass the fetched data to a StreamController and use that in the StreamBuilder, pagination works fine but Firestore doesn't send any updates to the device.
If I pass Firestore.instance.(...).snapshots() directly to the StreamBuilder (without using a StreamController), then I get updates from the server, but pagination is all screwed up. The UI is rebuilt and I'm sent to the top of the list.
Using StreamController
final int limit = 2;
final _list = List<DocumentSnapshot>();
final _listController = StreamController<List<DocumentSnapshot>>.broadcast();
DateTime startAt;
bool _isAllDataFetched = false;
Stream<List<DocumentSnapshot>> get listStream => _listController.stream;
void initState() {
super.initState();
_eventDao
?.getAllEventMedia(event?.id ?? "",
startAfter: startAt?.millisecondsSinceEpoch, limit: limit)
?.then((QuerySnapshot querySnapshot) {
_list.addAll(querySnapshot.documents);
_listController.sink.add(_list);
})?.catchError((error) {
print(error);
});
}
Widget build(BuildContext context) {
return StreamBuilder<List<DocumentSnapshot>>(
stream: listStream,
builder: (BuildContext context, AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
if (snapshot.hasError) {
return ErrorPage(imageAsset: AssetResources.failed);
}
if (snapshot.hasData &&
snapshot.data.isNotEmpty &&
snapshot.connectionState == ConnectionState.active) {
startAt = DateTime.fromMillisecondsSinceEpoch(
snapshot.data.last.data[EventMediaSchema.creationDate],
);
return MediaDisplayManagerAlt(
documents: snapshot.data,
atBottom: _fetchNextDocumentSet,
);
}
if (snapshot.hasData && snapshot.connectionState == ConnectionState.active) {
_isAllDataFetched = true;
}
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Text("NO MEDIA YET"),
),
],
),
);
},
);
void _fetchNextDocumentSet() async {
if (!_isAllDataFetched) {
QuerySnapshot querySnapshot = await _eventDao?.getAllEventMedia(event?.id ?? "",
startAfter: startAt?.millisecondsSinceEpoch, limit: limit);
_list.addAll(querySnapshot.documents);
_listController.sink.add(_list);
}
}
Passing snapshots to StreamBuilder
final int limit = 2;
DateTime startAt;
bool _isAllDataFetched = false;
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _eventDao?.(event?.id ?? "",
startAfter: startAt?.millisecondsSinceEpoch, limit: limit),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return ErrorPage(imageAsset: AssetResources.failed);
}
if (snapshot.hasData && snapshot.data.documents.isNotEmpty) {
return MediaDisplayManagerAlt(
documents: snapshot.data.documents,
atBottom: _fetchNextDocumentSet,
);
}
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Text("NO MEDIA YET"),
),
],
),
);
},
);
}
void _fetchNextDocumentSet() async {
if (!_isAllDataFetched) {
QuerySnapshot querySnapshot = await _eventDao?.getAllEventMedia(event?.id ?? "",
startAfter: startAt?.millisecondsSinceEpoch, limit: limit);
_list.addAll(querySnapshot.documents);
_listController.sink.add(_list);
}
}
It seems like these two approaches need to be combined in some way to get the desired effect, but after two days of trying I can't figure how to do that.

Resources