web-dev-qa-db-fra.com

Comment interroger un document Firestore dans Streambuilder et mettre à jour la vue de liste

J'essaie de récupérer des publications d'une collection Firestore appelée "publications", qui contient le ID utilisateur du créateur de publication et la description de la publication et cela est possible en utilisant à la fois StreamBuilder et FutureBuilder (Pas préférable, car il obtient un instantané une seule fois et ne se met pas à jour lorsqu'un champ change).

Cependant, je souhaite interroger une autre collection appelée "utilisateurs" avec le ID utilisateur du créateur du post et récupérer le document qui correspond à l'ID utilisateur.

Ce fut ma première approche:

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),
                        ),
                       )

Après avoir compris que ListView.builder () ne peut rendre que les éléments basés sur la liste DocumentSnapshot et ne peut pas gérer les requêtes à l'intérieur du générateur.

Après de nombreuses recherches: J'ai essayé de nombreuses alternatives comme, en essayant de construire la liste dans initState (), j'ai essayé d'utiliser le 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...
            );
          },
        );
      }
    );
  }
);

J'ai essayé avec Stream Group et je n'ai pas réussi à trouver un moyen de le faire, car il combine simplement deux flux, mais je veux que le deuxième flux soit récupéré par une valeur du premier flux.

Voici ma capture d'écran de la collection Firebase:

Collection "messages" de Firestore: Firestore "posts" Collection

Collection "utilisateurs" de Firestore: Firestore "users" collection

Je sais que c'est une chose très simple, mais je n'ai toujours pas trouvé de tutoriel ou d'articles pour y parvenir.

7
sanjay

J'ai posté une question similaire et trouvé plus tard une solution: rendre le widget renvoyé par l'itemBuilder avec état et y utiliser un FutureBuilder.

Requête supplémentaire pour chaque DocumentSnapshot dans StreamBuilder

Voici mon code. Dans votre cas, vous voudriez utiliser un nouveau widget avec état au lieu de ListTile, vous pouvez donc ajouter FutureBuilder pour appeler une fonction asynchrone.

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

}
2
Marcos

Publier pour ceux à l'avenir depuis que j'ai passé plusieurs heures à essayer de comprendre cela - en espérant que cela sauve quelqu'un d'autre.

Je recommande d'abord de lire sur Streams: https://www.dartlang.org/tutorials/language/streams Cela vous aidera un peu et sa courte lecture

La pensée naturelle est d'avoir un StreamBuilder imbriqué à l'intérieur du StreamBuilder extérieur, ce qui est bien si la taille du ListView ne changera pas en raison du StreamBuilder Réception de données. Vous pouvez créer un conteneur avec une taille fixe lorsque vous n'avez pas de données, puis rendre le widget riche en données lorsqu'il est prêt. Dans mon cas, je voulais créer un Card pour chaque document dans la collection "externe" et la collection "interne". Par exemple, j'ai une collection de groupes et chaque groupe a des utilisateurs. Je voulais une vue comme celle-ci:

[
  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,
]

L'approche imbriquée StreamBuilder a rendu les données, mais en faisant défiler le ListView.builder était un problème. Lors du défilement, je suppose que la hauteur a été calculée comme (group_header_card_height + inner_listview_no_data_height). Lorsque les données ont été reçues par le ListView interne, il a développé la hauteur de la liste pour s'adapter et les saccades. Son UX n'est pas acceptable.

Points clés de la solution:

  • Toutes les données doivent être acquises avant l'exécution de StreamBuilderbuilder. Cela signifie que votre Stream doit contenir des données des deux collections
  • Bien que Stream puisse contenir plusieurs éléments, vous voulez un Stream<List<MyCompoundObject>>. Commentaires sur cette réponse ( https://stackoverflow.com/a/53903960/608347 ) ont aidé

L'approche que j'ai adoptée était essentiellement

  1. Créer un flux de paires groupe-utilisateur

    une. Requête pour les groupes

    b. Pour chaque groupe, obtenez la liste d'utilisateurs appropriée

    c. Renvoyer une liste d'objets personnalisés enveloppant chaque paire

  2. StreamBuilder comme d'habitude, mais sur des objets group-to-userList au lieu de QuerySnapshots

À quoi cela pourrait ressembler

L'objet d'assistance composé:

class GroupWithUsers {
  final Group group;
  final List<User> users;

  GroupWithUsers(this.group, this.users);
}

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

essentiellement, "pour chaque groupe, créez une paire" gérant toutes les conversions de types

  Future<List<GroupWithUsers>> groupsToPairs(QuerySnapshot groupSnap) {
    return Future.wait(groupSnap.documents.map((DocumentSnapshot groupDoc) async {
      return await groupToPair(groupDoc);
    }).toList());
  }

Enfin, la requête interne réelle pour obtenir Users et construire notre assistant

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