How to query firestore document inside streambuilder and update the listview
Asked Answered
P

3

13

I'm trying to retrieve posts from a Firestore collection called "posts", which contains the post creator's userID and post description and this is possible by using both StreamBuilder and FutureBuilder(Not preferable, because it gets a snapshot only once and doesn't update when a field changes).

However, I want to query another collection called "users" with the post creator's userID and retrieve the document that matches the userId.

This was my first approach:

StreamBuilder<QuerySnapshot>(
  stream:Firestore.instance.collection("posts").snapshots(),
  builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
    if (!snapshot.hasData) {
      return Center(
        child: _showProgressBar,
      );
    }

   List<DocumentSnapshot> reversedDocuments = snapshot.data.documents.reversed.toList();
    return ListView.builder(
      itemCount: reversedDocuments.length,
      itemBuilder: (BuildContext context, int index){

        String postAuthorID = reversedDocuments[index].data["postAuthorID"].toString();
        String postAuthorName = '';
        Firestore.instance.collection("users")
        .where("userID", isEqualTo: postAuthorID).snapshots().listen((dataSnapshot) {
            print(postAuthorName = dataSnapshot.documents[0].data["username"]);
          setState(() {
            postAuthorName = dataSnapshot.documents[0].data["username"];                  
          });
        });

        String desc = reversedDocuments[index].data["post_desc"].toString();

        return new ListTile(
          title: Container(
            child: Row(
              children: <Widget>[
                Expanded(
                  child: Card(
                    child: new Column(
                      children: <Widget>[
                        ListTile(
                          title: Text(postAuthorName, //Here, the value is not changed, it holds empty space.
                            style: TextStyle(
                              fontSize: 20.0,
                            ),
                          ),
                          subtitle: Text(desc),
                        ),
                       )

After understanding that ListView.builder() can only render items based on the DocumentSnapshot list and can't handle queries inside the builder.

After many research: I tried many alternatives like, trying to build the list in the initState(), tried using the Nested Stream Builder:

return StreamBuilder<QuerySnapshot>(
  stream: Firestore.instance.collection('posts').snapshots(),
  builder: (context, snapshot1){
    return StreamBuilder<QuerySnapshot>(
      stream: Firestore.instance.collection("users").snapshots(),
      builder: (context, snapshot2){
        return ListView.builder(
          itemCount: snapshot1.data.documents.length,
          itemBuilder: (context, index){
            String desc = snapshot1.data.documents[index].data['post_description'].toString();
            String taskAuthorID = snapshot1.data.documents[index].data['post_authorID'].toString();
            var usersMap = snapshot2.data.documents.asMap();
            String authorName;
            username.forEach((len, snap){
              print("Position: $len, Data: ${snap.data["username"]}");
              if(snap.documentID == post_AuthorID){
                authorName = snap.data["username"].toString();
              }
            });
            return ListTile(
              title: Text(desc),
              subtitle: Text(authorName), //Crashes here...
            );
          },
        );
      }
    );
  }
);

Tried with Stream Group and couldn't figure out a way to get this done, since it just combines two streams, but I want the second stream to be fetched by a value from first stream.

This is my Firebase Collection screenshot:

Firestore "posts" collection: Firestore "posts" Collection

Firestore "users" collection: Firestore "users" collection

I know this is a very simple thing, but still couldn't find any tutorial or articles to achieve this.

Polygynous answered 3/10, 2018 at 22:55 Comment(4)
There's no need to call setState, this is in a build method. In general though, you should stay away from querying Firestore in a build method. Check out this excellent answer on keeping your build method pure.Unspeakable
Is there any alternative approach to query an user Id from the post and display their details, before the build is created?Polygynous
Why don't you put that query in your initState method?Unspeakable
Well, I need the post creator's id from the posts contained in the "posts" collection and then query that id with the "users" collection and then retrieve the user's name, picture or any other values. Now which should I be calling in the initStae method?Polygynous
B
5

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);
  }

}
Boycott answered 10/11, 2018 at 15:17 Comment(8)
Hey, thanks for responding, but, I'd prefer a StreamBuilder, since FutureBuilder gets called only once and I want my posts to be updated every time there's a change in the firestore.Polygynous
@Polygynous you would still use your main StreamBuilder. The FutureBuilder would be used to get the additional info that you need and would be triggered anytime the StreamBuilder detects a change in Firestore.Boycott
That might do, but I want to be able to perform a query from one collection(posts) document's value to fetch a document ID from another collection called users. Does your ChatMessageModel have any similar operation like that?Polygynous
@Polygynous that's what I'm doing. The StreamBuilder listens to new messages and when a new one comes in a new ChatMessage widget is created and its FutureBuilder loads the user data for the message's owner.Boycott
Can you post the ChatMessageModel code too, so that I can understand what's going on?Polygynous
There you go @PolygynousBoycott
Thanks Marcos, let me try this and see!Polygynous
I'm still working on this, but I'll update the answer once, I get my module working, I truly wanted to appreciate the time you took to help me out. I marked it as the answer, cheers!Polygynous
D
10

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 Streams: 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:

The approach I took was basically

  1. 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

  2. StreamBuilder as normal, but on group-to-userList objects instead of QuerySnapshots

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 Users 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);
    });
  }
Dormer answered 5/5, 2019 at 1:23 Comment(3)
Thanks a lot bro, you have save my life. This approach works for me with little modification.Nickell
Is there anyway you could post the full code example of this class? Having a bit of trouble figuring out where to place the StreamBuilders in this class.Level
This is not a bad approach but it has one downside - if a user is added to a group, your view will not update.Mcclurg
B
5

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);
  }

}
Boycott answered 10/11, 2018 at 15:17 Comment(8)
Hey, thanks for responding, but, I'd prefer a StreamBuilder, since FutureBuilder gets called only once and I want my posts to be updated every time there's a change in the firestore.Polygynous
@Polygynous you would still use your main StreamBuilder. The FutureBuilder would be used to get the additional info that you need and would be triggered anytime the StreamBuilder detects a change in Firestore.Boycott
That might do, but I want to be able to perform a query from one collection(posts) document's value to fetch a document ID from another collection called users. Does your ChatMessageModel have any similar operation like that?Polygynous
@Polygynous that's what I'm doing. The StreamBuilder listens to new messages and when a new one comes in a new ChatMessage widget is created and its FutureBuilder loads the user data for the message's owner.Boycott
Can you post the ChatMessageModel code too, so that I can understand what's going on?Polygynous
There you go @PolygynousBoycott
Thanks Marcos, let me try this and see!Polygynous
I'm still working on this, but I'll update the answer once, I get my module working, I truly wanted to appreciate the time you took to help me out. I marked it as the answer, cheers!Polygynous
M
0

Had the same problem and solved it by using nested StreamBuilders. If you don't expect the users data to change a lot and are fine fetching it only once, then you can replace the inner StreamBuilder with a FutureBuilder:

class PostList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: FirebaseFirestore.instance.collection('posts').snapshots(),
      builder: (context, postSnapshot) {
        if (postSnapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator(); // Loading indicator while waiting for post data
        }

        if (postSnapshot.hasError) {
          return Text('Error: ${postSnapshot.error}');
        }

        // Process the post data and build your UI
        List<PostWidget> postWidgets = postSnapshot.data!.docs.map((postDoc) {
          // Access post data
          String postText = postDoc['text'];

          // Access user reference from the post
          DocumentReference userRef = postDoc['post_creator_id'];

          // Using FutureBuilder for fetching user data once
          return FutureBuilder<DocumentSnapshot>(
            future: userRef.get(),
            builder: (context, userSnapshot) {
              if (userSnapshot.connectionState == ConnectionState.waiting) {
                return CircularProgressIndicator(); // Loading indicator while waiting for user data
              }

              if (userSnapshot.hasError) {
                return Text('Error: ${userSnapshot.error}');
              }

              // Access user data
              Map<String, dynamic> userData = userSnapshot.data!.data() as Map<String, dynamic>;

              return PostWidget(postText: postText, userData: userData);
            },
          );
        }).toList();

        return ListView(
          children: postWidgets,
        );
      },
    );
  }
}
Mcclurg answered 17/12, 2023 at 13:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.