Example code
toys = toysRef.where('postState', isEqualTo: 'recruiting').orderBy('createdAt', descending: true).snapshots();
fruits = fruitsRef.where('postState', isEqualTo: 'recruiting').orderBy('createdAt', descending: true).snapshots();
hi I'm trying to combine or merge two streams
I want to show toys and fruits order by desc with one StreamBuilder Widget
There is a common field ['createdAt']
I want to implement like this.
if some item added to firestore , it should show on realtime.
You can use StreamGroup from rxdart to merge your toys and fruits.
StreamGroup<List<QuerySnapshot>> combinedStreams = StreamGroup<List<QuerySnapshot>>.merge([
toysRef.where('postState', isEqualTo: 'recruiting').orderBy('createdAt', descending: true).snapshots(),
fruitsRef.where('postState', isEqualTo: 'recruiting').orderBy('createdAt', descending: true).snapshots()
]);
and to finally have them sorted in a stream, you could have:
StreamBuilder<List<QuerySnapshot>>(
stream: combinedStreams,
builder: (BuildContext context, AsyncSnapshot<List<QuerySnapshot>> snapshot) {
List<QueryDocumentSnapshot> combinedData = [];
for (QuerySnapshot snap in snapshot.data) {
combinedData.addAll(snap.docs);
}
combinedData.sort((a, b) => b['createdAt'].compareTo(a['createdAt']));
return ListView.builder(
itemCount: combinedData.length,
itemBuilder: (context, index) {
// Build your UI
}
);
}
)
You should use ListView.builder so that you have a performant list, and not render all the elements at once (would really lag your UI if you'd have a lot of elements).
Don't forget to check if snapshot has errors or is empty so that you can display a loader to the user or an error message.
Related
I am building an instant messaging app with flutter and firebase. I have a view to display user messages with a StreamBuider. The stream of the StreamBuider is accepting data from firebase. I have limited the number of messages loaded at a time for lazy loading purposes. The current problem is that when the user creates a new messages
For example, the original view has 3 messages (m1,m2,m3). When the user writes a new message (m4), the view will display (m2,m3,m4). m1 has gone. What I want is to keep all 4 messages. Is there a way to limit the number of documents gotten while listening on new documents?
StreamBuilder<QuerySnapshot>(
stream: messageService.getMessagesByChatIdStream(chatId),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
padding: EdgeInsets.all(10),
itemBuilder: (context, index) {
return buildMessageTile(index, snapshot.data?.docs[index]);
},
reverse: true,
itemCount: snapshot.data?.docs.length,
);
}
the getMessagesByChatIdStream() function is like this:
Stream<QuerySnapshot> getMessagesByChatIdStream(String chatId, {int limit = 5}) {
CollectionReference colRef =
firebaseFirestore.collection("messages").doc(chatId).collection(chatId);
return colRef
.limit(limit)
.orderBy('timestamp', descending: true)
.snapshots();
}
I have stored time stamp data in firebase, I am fetching it using streams and displaying it in the latest date order in a datatable widget. I used .orderBy to access descending bool. But neither true nor false worked, instead !snapshot.hasData condition is getting executed, Why orderBy is not working and what are the other queries that I can use to get the latest date order.
stream: FirebaseFirestore.instance
.collection('lender')
.doc(auth.currentUser!.email)
.collection('paymentData')
.where('name',
isEqualTo: Provider.of<UpdateNameProvider>(context,
listen: false)
.bname).orderBy('paidDate', descending: true)
.snapshots(),
//.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(
backgroundColor: Color(0xff8eacbb),
));
}
There are many possible reasons why a snapshot doesn't have data. At the very least you'll want to check if there's an error, and if so: log it somewhere.
Something like this:
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) { // 👈
return Text(snapshot.error); // 👈
}
...
Try like this.
FirebaseFirestore.instance
.collection('lender')
.doc(auth.currentUser!.email)
.collection('paymentData')
.orderBy('paidDate', descending: true).where('name',
isEqualTo: Provider.of<UpdateNameProvider>(context,
listen: false)
.bname)
.snapshots(),
I am using GetX and FutureBuilder to build a list of Cards from my DB.
Lets say I have 10 products in DB then 10 cards are shown. When I add one more product, the Cards on the HomeScreen aren't updated and I have to Navigate in and Out of page to show the 11th product.
How can I force the update i.e. probably make a "Refresh" button that may load the latest data.
PS: I do not want to use STREAM-BUILDER as I don't wish to listen to all changes actively but only when needed. I also cannot use SetState() as I am using GetX hence no StatefulWidget.
Here is my Card class:
FutureBuilder(
future: databaseController.getData()
builder: (context, snapshot) {
return StaggeredGridView.countBuilder(
itemCount: snapshot.data.length,
crossAxisCount: 2,
itemBuilder: (BuildContext context, int index) =>
GetX<FindDeviceLocation>(builder: (controller) {
return CreateTheCard(
lindex: index,
location: snapshot.data[index]["location"],
summary: snapshot.data[index]["summary"],
description: snapshot.data[index]["description"],
category: snapshot.data[index]["category"],
imageURL: snapshot.data[index]["adImage"],
onTapFunction: () => Get.to(DetailPage(
post: snapshot.data[index],
)));
}),
This is my method that fetches data from DB:
Future getData() async {
QuerySnapshot _firebaseDb = await FirebaseFirestore.instance
.collection("items")
.where("status", isEqualTo: true)
.orderBy("postTime", descending: true)
.get();
return _firebaseDb.docs;
}
The databaseController has a method call update(),so you can call databaseController.update() when you need to update your data.
Use the getx worker
Ever:
If data change you can update the view
ever(index, (value) {
// call your function here
// any code will be called any time index changes
});
I have the following stream builder:
streamCard() {
return StreamBuilder(
stream: cardsRef
.orderBy("timestamp", descending: true)
.limit(10)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return circularProgress();
}
List<CustomCard> cards = [];
snapshot.data.documents.forEach((doc) {
cards.add(CustomCard.fromDocument(doc));
});
...
return Stack(
alignment: Alignment.center,
children: cards,
);
I need to filter certain cards from being added to the stream/displayed when:
I'm the owner of the card ('ownerId' == currentUserId)
I've liked the card ('liked.' contains = currentUserId)
OwnerId is a field inside each document and Liked is an array with Id's who have liked it.
I've tried to remove the cards from being added to the cards List<> with .where and .contains, but couldn't properly 'discard' them. I was thinking another option could be to modify the Stack directly, in
children: cards
with cards.removeWhere/.skip, or something like that.
To follow the bloc pattern, business logic should happen in your bloc class. Which mean that you should do all the sorting or filtering in the bloc. When you add a new object into sink, the streambuilder will rebuild.
class BlaBloc {
final BehaviorSubject<List<String>> _results =
BehaviorSubject<List<String>>();
getResults() {
List<String> yourresults = yourapi.getResults();
_results.sink.add(yourresults);
};
getResultsLikedByMe() {
List<String> yourresults = yourapi.getResultsLikedByMe();
_results.sink.add(yourresults);
}
getResultsOwnerIsMe() {
List<String> yourresults = yourapi.getResultsOwnerIsMe();
_results.sink.add(yourresults);
}
BehaviorSubject<List<String>> get results => _results;
}
final blaBloc = BlaBloc();
When you build your streambuilder, point to your Bloc, for example as below:
body: StreamBuilder<List<String>>(
stream: blaBloc.results,
builder: (context, AsyncSnapshot<RecipesResponse> snapshot) {
// build your list here...
}
)
To understand more about Bloc pattern, this is a very useful tutorial you could follow here: RxDart Bloc Pattern
I have 2 Streams that I need to combine to build a widget, but unlike other questions I have seen I need to nest my streams.
I have a stream that gets a collection of documents from Firestore, and a stream that depends on data from the first to get a subcollection of documents. I would like to combine these into one stream, but they need to be nested since each document has its own subcollection of documents.
Stream 1 (Gets a collection of habits from FireStore):
Stream<List> getHabits(){
final Stream<QuerySnapshot> documents = Firestore.instance
.collection("users")
.document('VtL1sxOoCOdJaOTT87IbMRwBe282')
.collection("habits")
.snapshots();
Stream<List> data = documents.map((doc) {
List data;
final documents = doc.documents;
///Maybe this would work to get history of each doc?
for(int i = 0; i < documents.length; i++){
///not sure what to do
getHistory(documents[i].documentID, DateTime.utc(2019,7,7), DateTime.now());
}
data = documents.map((documentSnapshot) => documentSnapshot).toList();
return data;
});
return data;
}
Stream 2 (Called in Stream 1, Takes DocumentID as a parameter, gets sub-collection of documents):
Stream<List> getHistory(String id, DateTime start, DateTime end) async* {
await for (QuerySnapshot querySnapshot in Firestore.instance
.collection("users")
.document('VtL1sxOoCOdJaOTT87IbMRwBe282')
.collection("habits")
.document(id)
.collection("history")
.where('day', isGreaterThanOrEqualTo: start)
.where('day', isLessThanOrEqualTo: end)
.snapshots()) {
List history;
final documents = querySnapshot.documents;
history = documents.map((documentSnapshot) => documentSnapshot).toList();
yield history;
}
}
Any help on how I can combine these streams in a nested format into one stream to be used with StreamBuilder in flutter would be appreciated!'
EDIT
I am not sure if I am working in the right direction or not but I have tried to implement the solution from spenster and this is what I have at the moment in addition to the functions above.
StreamBuilder<List>(
stream: getHabits(),
initialData: [],
builder: (context, snapshot) {
List<UserHabit> habits = [];
List<Widget> test = List.generate(snapshot.data.length, (index){
List<History> history = [];
DocumentSnapshot doc = snapshot.data[index];
return StreamBuilder(
stream: getHistory(doc.documentID, DateTime.utc(2019,7,7), DateTime.now()),
builder: (context, snapshot) {
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Text('Loading...');
default:
if(!snapshot.data.isEmpty){ //history collection exists
for(int i = 0; i < snapshot.data.length; i++){
//add to history
history.add(History(
day: snapshot.data[i]['day'].toDate(),
dateCompleted: snapshot.data[i]['dateCompleted'].toDate(),
morning: snapshot.data[i]['morning'],
afternoon: snapshot.data[i]['afternoon'],
evening: snapshot.data[i]['evening'],
anytime: snapshot.data[i]['anytime'],
));
}
}
habits.add(UserHabit(
name: doc['habit'],
color: doc['color'],
icon: doc['icon'],
repeat: doc['repeat'],
daily: doc['daily'],
weekly: doc['weekly'],
monthly: doc['monthly'],
time: doc['time'],
history: history,
));
print(habits); //returns each iteration of assembling the list
return Text("i dont want to return anything");
}
},
);
}
);
print(habits); //returns empty list before anything is added
return Column(
children: test,
);
},
),
The Class for UserHabits and History can be shared, but they are just basic classes that assign types and allow easy access.
I have done something similar simply using nested StreamBuilders. Depending on how you want your Widgets organized, you can create streams within the outer StreamBuilder. Based on your clarifying comments, this is one possibility:
#override
Widget build(BuildContext context) {
var habits = Firestore.instance
.collection("users")
.document('VtL1sxOoCOdJaOTT87IbMRwBe282')
.collection("habits")
.snapshots();
return StreamBuilder<QuerySnapshot>(
stream: habits,
builder: (context, snapshot) {
if (!snapshot.hasData)
return Text("Loading habits...");
return ListView(children: snapshot.data.documents.map((document) {
var query = Firestore.instance
.collection("users")
.document('VtL1sxOoCOdJaOTT87IbMRwBe282')
.collection("habits")
.document(document.documentID)
.collection("history")
.where('day', isGreaterThanOrEqualTo: start)
.where('day', isLessThanOrEqualTo: end)
.snapshots();
return StreamBuilder<QuerySnapshot>(
stream: query,
builder: (context, snapshot) {
if (!snapshot.hasData) return Text("Loading...");
// right here is where you need to put the widget that you
// want to create for the history entries in snapshot.data...
return Container();
},
);
}).toList());
},
);
}
Try merging your streams with something like Observable.zip2(stream1,stream2,zipper) or Observable.combineLatest2(streamA, streamB, combiner).
For more info, check this post