How to query firestore document inside streambuilder and update the listview

前端 未结 2 840
借酒劲吻你
借酒劲吻你 2021-02-02 01:18

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

相关标签:
2条回答
  • 2021-02-02 01:43

    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:

    • All data should be acquired before StreamBuilder's builder execution. That means your Stream needs to contain data from both collections
    • Although Stream can hold multiple items, you want a Stream<List<MyCompoundObject>>. Comments on this answer (https://stackoverflow.com/a/53903960/608347) helped

    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);
        });
      }
    
    0 讨论(0)
  • 2021-02-02 02:03

    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);
      }
    
    }
    
    0 讨论(0)
提交回复
热议问题