Nesting two stream builders causing bad state error - firebase

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.

Related

Is there a way to other way of calling two collection in 1 stream builder?

I'm currently using stream builder and future builder to call two collections at the same time. I'm having hard time because the stream builder refreshes every time the database changes. Here's my source code:
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('thread')
.orderBy('published-time', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return snapshot.data!.docs.length > 0
? MediaQuery.removePadding(
removeTop: true,
context: context,
child: ListView(
shrinkWrap: true,
children: snapshot.data!.docs.map((DocumentSnapshot postInfo) {
return FutureBuilder<DocumentSnapshot>(
future: userCollection
.doc(postInfo.get('publisher-Id'))
.get(),
My variables are here:
final CollectionReference userCollection =
FirebaseFirestore.instance.collection('users');
final FirebaseAuth _auth = FirebaseAuth.instance;
Also tried calling two streambuilders:
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('thread')
.orderBy('published-time', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return snapshot.data!.docs.length > 0
? MediaQuery.removePadding(
removeTop: true,
context: context,
child: ListView(
shrinkWrap: true,
children: snapshot.data!.docs
.map((DocumentSnapshot postInfo) {
return StreamBuilder<DocumentSnapshot>(
stream: userCollection
.doc(postInfo.get('publisher-Id'))
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map<String, dynamic> userInfo =
snapshot.data!.data()
as Map<String, dynamic>;
It doesn't look like there is a better way of calling two collections, but you can achieve less rebuilds by considering some optiomization steps mentioned in this article:
Only wrap the widget that should rebuild during a stream change inside a StreamBuilder
Use the Stream.map to map your stream object into an object that your widget needs to show in UI.
Use the Stream.distinct to create a _DistinctStream in case your widget shouldn’t rebuild when the stream provides the same value in a
row.
Create a separate _DistinctStream for StreamBuilders on initState so that they can save streamed values first if your
streamController streams a new value before the screen's first
build.

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.

Error: Class '_JsonDocumentSnapshot' has no instance getter 'docs'

i am trying to get list of a particular uid document from firebase here is the code
StreamBuilder(
stream: firestore
.collection('interest')
.doc('${auth.currentUser.uid}')
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, index) {
return Container();
});
}),
Error:
Class '_JsonDocumentSnapshot' has no instance getter 'docs'.
Receiver: Instance of '_JsonDocumentSnapshot'
Tried calling: docs
my collection
i am try to get the list from this document uid
Thanks
I had a similar issue and I tried using explicit (BuildContext context, AsyncSnapshot snapshot) and it worked for me, also I create the listview without the builder:
ListView(
children: myList.map((object) {
return myWidget(object: object);
}).toList(),

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

How to efficiently access a firestore reference field's data in flutter?

Using similar code as flutter's firestore example, suppose there is a reference field stored in a snapshot document, called: document['userRef'].
First of all, how do I access the data of userRef? Using document['userRef'].get().data or document['userRef'].get().username I wasn't able to access the data. (NoSuchMethodError: Class 'Future<DocumentSnapshot>' has no instance getter 'data')
I also tried using document['userRef'].get().then(...) but getting the error: type 'Future<dynamic>' is not a subtype of type 'String'
Even if .then would work, wouldn't it then look up the same reference again for each message? Here the database is updated in realtime, but it's unnecessary to make the same lookup for multiple messages in the ListView.
class MessageList extends StatelessWidget {
MessageList({this.firestore});
final Firestore firestore;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: firestore.collection('messages').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return const Text('Loading...');
final int messageCount = snapshot.data.documents.length;
return ListView.builder(
itemCount: messageCount,
itemBuilder: (_, int index) {
final DocumentSnapshot document = snapshot.data.documents[index];
// document['userRef'] exists here
return ListTile(
title: Text(document['message'] ?? '<No message retrieved>'),
subtitle: Text('Message ${index + 1} of $messageCount'),
);
},
);
},
);
}
}
Edit:
I was able to fetch the nested data using FutureBuilder, though not sure how efficient it is. (Wouldn't this possibly send loads of redundant requests to Firebase?)
Creating a widget for the nested data, where document['userRef'] exists:
FutureBuilder(
future: userData(document['userRef']),
builder: (BuildContext context,
AsyncSnapshot<dynamic> uData) {
return Text(uData.data['username']);
},
);
And the userData function looks like this:
Future<dynamic> userData(DocumentReference user) async {
DocumentSnapshot userRef = await user.get();
return userRef.data;
}
Sticking to the Firebase and Flutter way, it is possible to use a Streambuilder inside a Streambuilder. That is, instead of using a FutureBuilder for the nested data, which makes you wait for each .get request.
(The code is untested, but the principle is tested.)
class MessageList extends StatelessWidget {
MessageList({this.firestore});
final Firestore firestore;
#override
Widget build(BuildContext context) {
Map UserSnapshot = Map(); // create a variable for accessing users by id
return StreamBuilder<QuerySnapshot>(
stream: firestore.collection('users').snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> UsersSnapshot) {
// process usersnapshot from list to map
UsersSnapshot.data.documents.forEach((userRecord) {
//print(optionRecord.documentID); // debug
UserSnapshot[userRecord.documentID] = userRecord;
});
// user data can be accessed as soon as there is a reference field or documentID:
// UserSnapshot[document['userRef']]['userName'}
return StreamBuilder<QuerySnapshot>(
stream: firestore.collection('messages').snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> MessagesSnapshot) {
if (!MessagesSnapshot.hasData) return const Text('Loading...');
final int messageCount = MessagesSnapshot.data.documents.length;
return ListView.builder(
itemCount: messageCount,
itemBuilder: (_, int index) {
final DocumentSnapshot document =
MessagesSnapshot.data.documents[index];
// document['userRef'] exists here
// UserSnapshot[document['userRef']]['userName'} is accessible here
return ListTile(
title:
Text(document['message'] ?? '<No message retrieved>'),
subtitle: Text('Message ${index + 1} of $messageCount'),
);
},
);
},
);
});
}
}

Resources