How to query firestore document inside streambuilder and update the listview
I posted a similar question and later found a solution: make the widget returned by the itemBuilder stateful and use a FutureBuilder in it.
Additional query for every DocumentSnapshot within StreamBuilder
Here's my code. In your case, your would want to use a new Stateful widget instead of ListTile, so you can add the FutureBuilder to call an async function.
StreamBuilder(
stream: Firestore.instance
.collection("messages").snapshots(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(
child: PlatformProgressIndicator(),
);
default:
return ListView.builder(
reverse: true,
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) {
List rev = snapshot.data.documents.reversed.toList();
ChatMessageModel message = ChatMessageModel.fromSnapshot(rev[index]);
return ChatMessage(message);
},
);
}
},
)
class ChatMessage extends StatefulWidget {
final ChatMessageModel _message;
ChatMessage(this._message);
@override
_ChatMessageState createState() => _ChatMessageState(_message);
}
class _ChatMessageState extends State<ChatMessage> {
final ChatMessageModel _message;
_ChatMessageState(this._message);
Future<ChatMessageModel> _load() async {
await _message.loadUser();
return _message;
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
child: FutureBuilder(
future: _load(),
builder: (context, AsyncSnapshot<ChatMessageModel>message) {
if (!message.hasData)
return Container();
return Row(
children: <Widget>[
Container(
margin: const EdgeInsets.only(right: 16.0),
child: GestureDetector(
child: CircleAvatar(
backgroundImage: NetworkImage(message.data.user.pictureUrl),
),
onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) =>
ProfileScreen(message.data.user)));
},
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
message.data.user.name,
style: Theme.of(context).textTheme.subhead,
),
Container(
margin: const EdgeInsets.only(top: 5.0),
child: _message.mediaUrl != null
? Image.network(_message.mediaUrl, width: 250.0)
: Text(_message.text))
],
),
)
],
);
},
),
);
}
}
class ChatMessageModel {
String id;
String userId;
String text;
String mediaUrl;
int createdAt;
String replyId;
UserModel user;
ChatMessageModel({String text, String mediaUrl, String userId}) {
this.text = text;
this.mediaUrl = mediaUrl;
this.userId = userId;
}
ChatMessageModel.fromSnapshot(DocumentSnapshot snapshot) {
this.id = snapshot.documentID;
this.text = snapshot.data["text"];
this.mediaUrl = snapshot.data["mediaUrl"];
this.createdAt = snapshot.data["createdAt"];
this.replyId = snapshot.data["replyId"];
this.userId = snapshot.data["userId"];
}
Map toMap() {
Map<String, dynamic> map = {
"text": this.text,
"mediaUrl": this.mediaUrl,
"userId": this.userId,
"createdAt": this.createdAt
};
return map;
}
Future<void> loadUser() async {
DocumentSnapshot ds = await Firestore.instance
.collection("users").document(this.userId).get();
if (ds != null)
this.user = UserModel.fromSnapshot(ds);
}
}
Posting for those in the future since I spent several hours trying to figure this out - hoping it saves someone else.
First I recommend reading up on Stream
s: https://www.dartlang.org/tutorials/language/streams
This will help a bit and its a short read
The natural thought is to have a nested StreamBuilder
inside the outer StreamBuilder
, which is fine if the size of the ListView
wont change as a result of the inner StreamBuilder
receiving data. You can create a container with a fixed size when you dont have data, then render the data-rich widget when its ready. In my case, I wanted to create a Card
for each document in both the "outer" collection and the "inner" collection. For example, I have a a Group collection and each Group has Users. I wanted a view like this:
[
Group_A header card,
Group_A's User_1 card,
Group_A's User_2 card,
Group_B header card,
Group_B's User_1 card,
Group_B's User_2 card,
]
The nested StreamBuilder
approach rendered the data, but scrolling the ListView.builder
was an issue. When scrolling, i'm guessing the height was calculated as (group_header_card_height
+ inner_listview_no_data_height
). When data was received by the inner ListView
, it expanded the list height to fit and the scroll jerks. Its not acceptable UX.
Key points for the solution:
- All data should be acquired before
StreamBuilder
'sbuilder
execution. That means yourStream
needs to contain data from both collections - Although
Stream
can hold multiple items, you want aStream<List<MyCompoundObject>>
. Comments on this answer (https://stackoverflow.com/a/53903960/608347) helped
The approach I took was basically
Create stream of group-to-userList pairs
a. Query for groups
b. For each group, get appropriate userList
c. Return a List of custom objects wrapping each pair
StreamBuilder
as normal, but on group-to-userList objects instead ofQuerySnapshot
s
What it might look like
The compound helper object:
class GroupWithUsers {
final Group group;
final List<User> users;
GroupWithUsers(this.group, this.users);
}
The StreamBuilder
Stream<List<GroupWithUsers>> stream = Firestore.instance
.collection(GROUP_COLLECTION_NAME)
.orderBy('createdAt', descending: true)
.snapshots()
.asyncMap((QuerySnapshot groupSnap) => groupsToPairs(groupSnap));
return StreamBuilder(
stream: stream,
builder: (BuildContext c, AsyncSnapshot<List<GroupWithUsers>> snapshot) {
// build whatever
});
essentially, "for each group, create a pair" handling all the conversion of types
Future<List<GroupWithUsers>> groupsToPairs(QuerySnapshot groupSnap) {
return Future.wait(groupSnap.documents.map((DocumentSnapshot groupDoc) async {
return await groupToPair(groupDoc);
}).toList());
}
Finally, the actual inner query to get User
s and building our helper
Future<GroupWithUsers> groupToPair(DocumentSnapshot groupDoc) {
return Firestore.instance
.collection(USER_COLLECTION_NAME)
.where('groupId', isEqualTo: groupDoc.documentID)
.orderBy('createdAt', descending: false)
.getDocuments()
.then((usersSnap) {
List<User> users = [];
for (var doc in usersSnap.documents) {
users.add(User.from(doc));
}
return GroupWithUser(Group.from(groupDoc), users);
});
}