Using FutureBuilder inside StreamBuilder - firebase

I am trying to listen for changes in specific user document in 'users' Firestore and get from this document avatarPath. When I get it I want to request for download url of user's avatar (specific avatarPath) in Storage. I can get those data (avatarPath and download url) but FutureBuilder isn't executed and finally it returns Text('avk') in StreamBuilder.
Is there any way to build avatar with just only one Builder function/query? or maybe is there some functionality I don't know
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('users')
.where('uid',
isEqualTo: currentUser!.uid)
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
final storageRef =
FirebaseStorage.instance.ref();
final avatarPath = snapshot
.data!.docs[0]
.get('avatarPath');
FutureBuilder(
future: storageRef
.child(avatarPath)
.getDownloadURL(),
builder: (context, snapshot) {
return SizedBox(
width: 128,
height: 128,
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 3,
),
borderRadius:
BorderRadius.circular(
100),
),
child: Image.network(
snapshot.data.toString()),
),
);
},
);
}
return Text("avk");
}),

You're loading documents for the users from Firestore, which is an asynchronous operation. Then you load an image for each user from Cloud Storage, which is a separate asynchronous operation. Using separate builders for these (a StreamBuilder for the documents, and a FutureBuilder for each image) seems correct to me, as each asynchronous operation has its own lifecycle and the builder wrap that for you.
Also see:
How do I join data from two Firestore collections in Flutter?

Related

Does Streambuilder fetches all the data from a firestore document or only fields that are needed?

In my App I use a Streambuilder to show a list of plans that are stored in Firestore (every plan has its own document) on a screen:
final CollectionReference planCol = FirebaseFirestore.instance.collection('users').doc(FirebaseAuth.instance.currentUser.uid).collection('plans');
body: StreamBuilder(
stream: planCol.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final planDocs = snapshot.data.docs;
return ListView.builder(
itemCount: planDocs.length,
itemBuilder: (context, index) => MyListTile(
title: Text(planDocs[index]['name']),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () {
edit(planDocs[index]);
},
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ExerciseTable(
plan: UserPlan(
name: planDocs[index]['name'],
exerciseNames: planDocs[index]
['exerciseNames'],
rows: planDocs[index]['rows'],
planId: planDocs[index].id,
),
);
},
),
);
}),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}));
On this screen, I only want to show the name of each document and not all the other data which is stored in the document(because they are very big). Now I was asking myself how efficient Streambuilder works when each document is very big. Does it load the whole document or only the field of the document, that is needed?
On this screen I only want to show the name of each document and not all the other data which is stored in the document(because they are very big).
All Cloud Firestore listeners fire on the document level. So there is no way you can get only the value of a single field in a document. That's the way Firestore works.
Now I was asking myself how efficient Streambuilder works when each document is very big.
It's more about the time that it takes to download those documents.
Does it load the whole document or only the field of the document, that is needed?
It loads the entire document.
If you need only some names, you should consider storing those names in a single document. In this case, you can read only one document and you have to pay only one read.

Do cloud firestore snapshot reads all the documents in the collection?

I'm creating a chat screen. What I'm currently doing is that I'm using a Streambuilder to listen to the 'messages' collection and display the messages using ListView.builder().
Below is the code i'm using.
StreamBuilder<QuerySnapshot>(
stream: _fireStoreInstance
.collection('$collectionName/$docID/messages')
.orderBy('sentAt', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return Center(
child: CircularProgressIndicator(),
);
List<Map> documents = snapshot.data.docs
.map((doc) => {'documentId': doc.id, ...doc.data()})
.toList();
return ListView.builder(
cacheExtent: MediaQuery.of(context).size.height,
reverse: true,
itemCount: documents.length,
padding:
const EdgeInsets.only(left: 15.0, right: 15.0, bottom: 5.0),
itemBuilder: (context, index) {
return MessageBubble(
...
);
},
);
},
),
My concern is, will the query fetch all the documents in the collection all at once? If yes then it will be a lot of reads each time the query is executed
_fireStoreInstance
.collection('$collectionName/$docID/messages')
.orderBy('sentAt', descending: true)
.snapshots();
Do I need to paginate by using limit ? If I paginate how do I listen to new messages ? Thank you for your help.
Yes, .snapshots() will read and keep listening to all documents that fit the query, if you want a subset of that you will have to paginate it using .limit().
I have found this article, with a video step by step on How to perform real-time pagination with Firestore with the use of an infinite scroll. I think this is exactly what you looking for, so I won't post any code since you can follow that example.

I can't use orderBy query from firestore if where is used Flutter

I am retreiving a list from Firestore and I need to use query where to retrieve specific UserID but at the same time I need also to order this list by the timestamp.
Using orderBy and where is not possible to get this is my code:
Container(
height: MediaQuery.of(context).size.height * 0.275,
child: StreamBuilder(
stream: FirebaseFirestore.instance
.collection('ConfirmedCart').orderBy('cartDate',descending: false)
.where('idConsultant', isEqualTo: firebaseUser.uid)
.snapshots(),
builder: (context, snapshotConfirmedCart) {
if (!snapshotConfirmedCart.hasData)
return Container(
width: 40,
child: Center(
child: CircularProgressIndicator(
backgroundColor: Colors.orange,
),
));
as you can see above orderBy is used and can't fetch data, If I removed orderBy, I get the data but without my specific order.
FirebaseFirestore.instance
.collection('ConfirmedCart')
.where('idConsultant', isEqualTo: firebaseUser.uid)
.orderBy('cartDate',descending: false)
When performing compound queries you must create a composite index in the firebase console. When the above query is executed you will get a link in the log that will take you to the console to create the index.

Correct use of Streams with Flutter-Listview

I am trying to display a realtime chat-screen in flutter with with firebase-firestore (equal to the homescreen of whatsapp).
Working: Creating a list of all the contacts "peers". Have a Look at my Listview:
Container(
child: StreamBuilder(
stream:
//FirebaseFirestore.instance.collection('users').snapshots(),
FirebaseFirestore.instance
.collection('users')
.doc(currentUserId)
.collection('peers')
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(themeColor),
),
);
} else {
return ListView.builder(
padding: EdgeInsets.all(10.0),
itemBuilder: (context, index) =>
buildItem(context, snapshot.data.documents[index]),
itemCount: snapshot.data.documents.length,
);
}
},
),
),
not working: Loading specific data for each tile like last message or name. I cant query this at the time of creating my first list (first query returns peer-ids, second returns userdata of a peer-id). My buildItem method consists of another streambuilder, however, as soon as the first streambuilder makes changes, the app freezes.
Widget buildItem(BuildContext context, DocumentSnapshot document) {
return StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance
.collection('users')
.doc(document.data()['peerId'])
.snapshots(),
builder: ...
Is this the proper way to nest streams? Simple Listviews are documented quite well, but i couldn't find a good example on this on google. Any help is appreciated.
Try creating your stream just once in initState and pass it onto this method:
//in initState
peersStream = FirebaseFirestore.instance
.collection('users')
.doc(currentUserId)
.collection('peers')
.snapshots(),
Then use stream: peersStream in the StreamBuilder.
Also, it is recommended to use widget-classes over methods for widgets: https://stackoverflow.com/a/53234826/5066615

Nesting two stream builders causing bad state error

I am fetching data from two different firestore collections and this is my code
StreamBuilder(
stream: Firestore.instance.collection('items').snapshots(),
builder: (BuildContext context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return CupertinoActivityIndicator();
}
if(snapshot.data != null){
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context,index){
return Column(
children: <Widget>[
Text(snapshot.data.documents[index]['name']),
Text(snapshot.data.documents[index]['color']),
Text(snapshot.data.documents[index]['lifetime']),
Container(
child: StreamBuilder(
stream: Firestore.instance.collection('users')
.document(userid).collection('Quantity')
.document(snapshot.data.documents[index]['id']).snapshots(),
builder: (BuildContext context, snap){
if(snapshot.connectionState == ConnectionState.waiting){
return CupertinoActivityIndicator();
}
if(snap.data != null){
return Container(
child: Text(snap.data.documents.length)
);
}
},
),
)
],
);
});
}
},
)
It is giving me error but when I use futurebuilder inside streambuilder everything works fine and I also used stream broadcast but it is also giving me same error.
Here is the code which I used for broadcast stream
StreamController _controller = StreamController.broadcast();
Stream getItems() async*{
Firestore.instance.collection('items').snapshots().listen((data){
_controller.add(data);
})
yield* _controller.stream;
}
You shouldn't create a new Stream inside the StreamBuilder. When you do:
StreamBuilder(
stream: Firestore.instance.collection('items').snapshots(),
And
StreamBuilder(
stream: Firestore.instance.collection('users')
.document(userid).collection('Quantity')
.document(snapshot.data.documents[index]['id']).snapshots(),
Each time your build() function is called a new StreamBuilder is created, so Firestore.instance.collection()...snapshots() is called, returning a new Stream each time.
You should convert your widget to a StatefulWidget and initialize your Stream on initState(), passing it as a class variable to your StreamBuilder. The nested StreamBuilder can also be transformed into a StatefulWidget and created in place, but initialized on the same manner. Just pay attention that you might need a Key for showing it correctly on a ListView.
Also if you want to convert a Single Subscription Stream to a Broadcast Stream you just have to call asBroadcastStream to convert it.

Resources