Flutter querying different levels of data in Streambuilder - flutter

I am trying to access two levels of information in Firebase. The top level collection is called messages and the second level is called chats. I am having trouble accessing the data snapshot retrieved of the Chats collection. Here is a screen print of my collections:
And here please find the code:
return StreamBuilder<QuerySnapshot>(
stream: _firestore
.collection('messages')
.where("senderId", isEqualTo: currentUserId)
.where("receiverId", isEqualTo: member.id)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(
backgroundColor: Colors.lightBlueAccent,
),
);
}
final messageHeaders = snapshot.data!.docs.reversed;
print("messages first = ${messageHeaders.first.id}");
List<MessageBubble> messageBubbles = [];
for (var messageHeader in messageHeaders) {
print(messageHeader["senderId"]);
for (var messageDetail in messageHeader["chats"]) {
print(messageDetail["text"]);
}
So in the print statements above. The one to print the "senderId" is working fine. But not the one to print the "text"
Any help with this would be gratefully received.

Firestore queries are shallow, meaning that you don't get the entire subtree when you query a document. In your case, chats is a subcollection, therefore it will not be queried with the document.
So after getting the document snapshot, you have to make another Firestore query to get chats subcollection for that specific document in messages collection.

Related

Does Streambuilder store the data after app restarts?

I have a simple streambuilder that reads the users document, and I use it to show some of the user's data. My question is, would this streambuilder re-read the document everytime the user restarts the app? If, yes is there any way to prevent the streambuilder from re-reading it everytime the user restarts the app unless there is a change in the document?
StreamBuilder(
stream: _firestore
.collection('users')
.doc(_auth.currentUser!.uid)
.snapshots(),
builder:
(context, AsyncSnapshot<DocumentSnapshot<Object?>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(
color: isDarkMode ? Colors.white : Colors.black,
);
}
if (snapshot.hasData) {
if (snapshot.data!.exists) {
snapshot.data!['serviceEnabled'] == true
? startServices()
: null;
return Center(
child: Column(
This streambuilder is on the homepage of the app, I show some of the user's data on the homepage.
How would the database know whether there's a change in the document without reading that document?
If you can answer that, you can probably write a query to match that same condition.
For example, if each document has a lastUpdated field, you could just get the updated document with:
_firestore
.collection('users')
.where('lastUpdated', '>', timestampWhenYouLastReadDocuments)
.get()
Aside from that query to update the cache, you could then get the documents from the cache in other places in your app.

Get stream of results from Firebase Realtime Database inside nested array inside nested document in Flutter

I am currently developing a chatting page on my mobile application and was using Realtime Database to reduce costs. Not having much Realtime Database experience, I am running into issues developing a page for active chatrooms where each user has a document with their uid where there is an array of chats . How can I generate a list of the first names based on the items inside of the array of chats.
I realize it's best practice to show code you have tried, however, I have only used Firebase Firestore and have no idea where to start, considering the complexity of the structure
You can try this answer.
Here I have created StreamBuilder for getting data once. As I have provided, Stream outside of StreamBuilder means it will create a stream only once.
final _snap = FirebaseDatabase.instance
.ref()
.child(`users/${uid}`)
StreamBuilder<Event>(
stream: _snap.onValue,
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
DataSnapshot snap = snapshot.data as DataSnapshot;
List<dynamic> chatArray = snap.value['chats'];
List<String> firstNames = [];
for (var chat in chatArray) {
String firstName = chat[recepientFirstName];
firstNames.add(firstName);
}
return ListView.builder(
itemCount: firstNames.length,
itemBuilder: (context, index) {
return Text(firstNames[index]);
},
);
} else {
return Text('Loading...');
}
},
)
Found similar reference about this Questions so you can also visit these threads:
Thread1
Thread2
Thread3

Flutter + Firestore chat .. Listview rebuilds all items

I'm trying to add chat functionality to my app (kind of similar to WhatsApp functionalities), using flutter and Firestore. The main structure on Firestore is to have 2 collections (I want the unread message count as well):
users: and each user will have a subcollection "chats" that will include all CHATS_IDs. This will be the main place to build home chat page (shows a history list of all chats) by getting the user chat list.
chats: a list of all chats and each chat document has a subcollection of messages.
My main issue is in building the home page (where a list of all user previous chats should appear). I get/subscribe the user chat subcollection, and for each chat ID listed in there I also subscribe for the chat itself in the chat collection (using the ID).
Here are screenshots of it in principle:
users collection:
chats coleection:
and here is the main screen of interest (principle from whatsapp screen):
What I'm doing is that I retrieve user's chat subcollection (and register a listener to it using StreamBuilder), and also for number of unread messages/last message and last message time, I subscribe to listen for each of these chats (and want to use each user last message time, status and his last presence in that chat doc to calculate the unread count) .
The problem is that Listview.builder rebuilds all items (initially and on scroll) instead of just the viewed ones. here is my code:
Stream<QuerySnapshot> getCurrentUserChats(userId) {
return FirebaseFirestore.instance
.collection(AppConstants.USERS_COLLECTION)
.doc('$userId')
.collection(AppConstants.USER_CHATS_SUBCOLLECTION)
.orderBy('lastMsgTS', descending: true)
.snapshots()
.distinct();
}
Widget getRecentChats(userId) {
return StreamBuilder<QuerySnapshot>(
stream: getCurrentUserChats(userId),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.docs.isNotEmpty) {
print('snapshot of user chats subcoll has changed');
List<QueryDocumentSnapshot> retrievedDocs = snapshot.data.docs;
return Container(
height: 400,
child: ListView.builder(
//childrenDelegate: SliverChildBuilderDelegate(
itemCount: snapshot.data.size,
itemBuilder: (context, index) {
String chatId = retrievedDocs[index].id;
print('building index: $index, chatId: $chatId');
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection(AppConstants.CHATS_COLLECTION)
.doc('$chatId')
.snapshots()
.distinct(),
builder:
(context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasData) {
print('${snapshot.data?.id}, isExist: ${snapshot.data?.exists}');
if (snapshot.data.exists) {
return KeyProxy(
key: ValueKey(chatId),
child: ListTile(
leading: CircleAvatar(
child: Container(
//to be replaced with user image
color: Colors.red,
),
),
title: Text('$chatId'),
subtitle: Text(
"Last Message received on: ${DateTimeUtils.getDateViewFromDT(snapshot.data.data()['ts']?.toDate())}"),
),
);
}
}
return SizedBox.shrink();
},
);
},
/*childCount: snapshot.data.size,
findChildIndexCallback: (Key key) {
print('calling findChildIndexCallback');
final ValueKey valKey = key;
final String docId = valKey.value;
int idx = retrievedDocs.indexOf(retrievedDocs
.where((element) => element.id == docId)
.toList()[0]);
print('docId: $docId, idx: $idx');
return idx;
}*/
),
);
}
return Center(child: UIWidgetUtils.loader());
});
}
After searching, I found these related suggestions (but both didn't work):
A github issue suggested thesince the stream is reordarable (github: [https://github.com/flutter/flutter/issues/58917]), but even with using ListView.custom with a delegate and a findChildIndexCallback, the same problem remained.
to use distinct.
But removing the inner streambuilder and just returning the tiles without a subscription, makes the ListView.builder work as expected (only builds the viewed ones). So my questions are:
Why having nested stream builders causing all items to be rebuil.
is there a better structure to implement the above features (all chats with unread count and last message/time in real-time). Especially that I haven't added lazy loading yet. And also with this design, I have to update multiple documents for each message (in chats collection, and each user's subcollection).
Your help will be much appreciated (I have checked some other SO threads and medium articles, but couldn't find one that combines these features in one place with and preferably with optimized design for scalability/price using Firestore and Flutter).
I think that You can do this:
Widget build(ctx) {
return ListView.builder(
itemCount: snapshot.data.size,
itemBuilder: (index, ctx) =>_catche[index],
)
}
and for _catche:
List<Widget> _catche = [/*...*/];
// initialize on load

Retrieve data with StreamBuilder from collection and subCollection in same time Flutter cloud Firestore

I have a collection called UserCollection,
into this collection I have a subcollection with other specific documents called 'UserSubCollection'
with StreamBuilder I am trying to get these collections unsuccesfully
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('UserCollection')
.doc(firebaseUser.uid).collection('UserSubCollection')
.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return Center(child: Text("Error"));
else if (snapshot.data.docs.isEmpty) //also check if empty! show loader?
return Center(child: Text('Errors'));
else
return new ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (BuildContext context, int index) {
I am able to get just one or a collection or the subcollection.
there is a way to get them both?
First question: do your items need to update automatically over time? Otherwise, I suggest you a FutureBuilder instead of a StreamBuilder.
To get both I suggest you using a separate method like this:
Future<List</*INSERT RETURN TYPE*/>> _retrieveUsers() async{
//get all the users
QuerySnapshot query = await FirebaseFirestore.instance
.collection('UserCollection').get();
Map<String,Map<String,dynamic>> userMap = Map();
//add each user to the map
query.docs.forEach(doc => userMap.putIfAbsent(doc.id, doc.data());
FutureGroup<QuerySnapshot> queries = FutureGroup(); //add async package in pubspec.yaml [there might be version issues]
//retrieve sub collections and bind them to the corresponding user
query.docs.forEach((doc) => queries.add(_retrieveSubCollection(userMap,doc)));
//Close queries
queries.close();
//Await for all the queries to be completed
await queries.future;
//now you have for each user a structure containing its data plus everything in its sub collection. Do whatever you want and then return a list or a map according to what you need
return /*YOUR RETURN*/;
}
Future<Void> _retrieveSubCollection( Map<String,Map<String,dynamic>> userMap, DocumentSnapshot doc) async{
//retrieve the subcollection of a document using its id
QuerySnapshot query = query FirebaseFirestore.instance
.collection('UserCollection')
.doc(doc.id).collection('UserSubCollection').get();
//bind the subcollection to the relative user
Map<String,dynamic> userData = userMap[doc.id];
userData.add("subcollection", query.docs.map((subDoc) =>subDoc.data()).toList());
userMap.update(doc.id,(value)=> userData);
return;
}
Now just use a future builder to display the result of your queries.
FutureBuilder(future: _retrieveUsers(), builder: (context,snapshot) {/*YOUR CODE*/}
I actually didn't try the code in a real environment, so if you find issues on updating the user map there might be problems due to asynchronous updates. However, since every query target a specific element in the map, I don't think there will be issues

Firestore Query Document's Subcollection

I'm learning Flutter/Dart and trying to connect to my Firestore database. I want to query a collection for a document, then a collection within that document. I'm still learning Dart but what I've found is that async stream building should be the best method.
I have tried db.instance.collect('').document('').collection('').snapshot() but streambuilder doesn't seem to support this and I get the error
The method 'collection' isn't defined for the type 'Query'.
I have to end my query after db.instance.collection('').where('name' isEqualto: 'Name').snapshots()
Below is an example.
My goal is to query all of the classes and present them in a list.
Example - Teachers <-- 1st
Collection   Teacher Name <--Document within collection
  Classes <---Collection 2    Class1 <-- Query This    Class2 <---Query
This
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class ClassListPage extends StatelessWidget{
#override
Widget build(BuildContext context){
return Material(
child: new StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('Teachers').
where('name', isEqualTo: 'Dr. Who')
//.collection('Classes') <--This is not working
.snapshots(),
//HERE is where I want to query for the 'classes' collection then query
//for the documents within
builder:(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if(!snapshot.hasData)return new Text('..Loading');
return new ListView(
children: snapshot.data.documents.map((document){
return new ListTile(
title: new ListTile(
title: new Text(document['name']),
),
);
}).toList(),
);
}
),
);
}
}
In your question, you say that you're using db.instance.collection('').document('').collection('').snapshot(), but in your code, there is no call to document(). Here's what I see:
Firestore.instance
.collection('Teachers')
.where('name', isEqualTo: 'Dr. Who')
.collection('Classes')
.snapshots()
This isn't going to work because where() returns a Query, and Query doesn't have a collection() method. It sounds like what you need to do instead is execute that query, look at the documents in the result set (there could be any number, not just 1), then make anoterh query for each document's subcollections.