Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I join data from two Firestore collections in Flutter?

Tags:

I have a chat app in Flutter using Firestore, and I have two main collections:

  • chats, which is keyed on auto-ids, and has message, timestamp, and uid fields.
  • users, which is keyed on uid, and has a name field

In my app I show a list of messages (from the messages collection), with this widget:

class ChatList extends StatelessWidget {   @override   Widget build(BuildContext context) {     var messagesSnapshot = Firestore.instance.collection("chat").orderBy("timestamp", descending: true).snapshots();     var streamBuilder = StreamBuilder<QuerySnapshot>(           stream: messagesSnapshot,           builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> querySnapshot) {             if (querySnapshot.hasError)               return new Text('Error: ${querySnapshot.error}');             switch (querySnapshot.connectionState) {               case ConnectionState.waiting: return new Text("Loading...");               default:                 return new ListView(                   children: querySnapshot.data.documents.map((DocumentSnapshot doc) {                     return new ListTile(                       title: new Text(doc['message']),                       subtitle: new Text(DateTime.fromMillisecondsSinceEpoch(doc['timestamp']).toString()),                     );                   }).toList()                 );             }           }         );         return streamBuilder;   } } 

But now I want to show the user's name (from the users collection) for each message.

I normally call that a client-side join, although I'm not sure if Flutter has a specific name for it.

I've found one way to do this (which I've posted below), but wonder if there is another/better/more idiomatic way to do this type of operation in Flutter.

So: what is the idiomatic way in Flutter to look up the user name for each message in the above structure?

like image 778
Frank van Puffelen Avatar asked Nov 27 '19 00:11

Frank van Puffelen


People also ask

How do I display data from firestore in flutter?

There should be array called contacts and inside that there should be 3 maps according to your data. Then create a list in your screen class. List contacts; Then create a function to retrieve data from firestore.


2 Answers

You can do it with RxDart like that.. https://pub.dev/packages/rxdart

import 'package:rxdart/rxdart.dart';  class Messages {   final String messages;   final DateTime timestamp;   final String uid;   final DocumentReference reference;    Messages.fromMap(Map<String, dynamic> map, {this.reference})       : messages = map['messages'],         timestamp = (map['timestamp'] as Timestamp)?.toDate(),         uid = map['uid'];    Messages.fromSnapshot(DocumentSnapshot snapshot)       : this.fromMap(snapshot.data, reference: snapshot.reference);    @override   String toString() {     return 'Messages{messages: $messages, timestamp: $timestamp, uid: $uid, reference: $reference}';   } }  class Users {   final String name;   final DocumentReference reference;    Users.fromMap(Map<String, dynamic> map, {this.reference})       : name = map['name'];    Users.fromSnapshot(DocumentSnapshot snapshot)       : this.fromMap(snapshot.data, reference: snapshot.reference);    @override   String toString() {     return 'Users{name: $name, reference: $reference}';   } }  class CombineStream {   final Messages messages;   final Users users;    CombineStream(this.messages, this.users); }  Stream<List<CombineStream>> _combineStream;  @override   void initState() {     super.initState();     _combineStream = Observable(Firestore.instance         .collection('chat')         .orderBy("timestamp", descending: true)         .snapshots())         .map((convert) {       return convert.documents.map((f) {          Stream<Messages> messages = Observable.just(f)             .map<Messages>((document) => Messages.fromSnapshot(document));          Stream<Users> user = Firestore.instance             .collection("users")             .document(f.data['uid'])             .snapshots()             .map<Users>((document) => Users.fromSnapshot(document));          return Observable.combineLatest2(             messages, user, (messages, user) => CombineStream(messages, user));       });     }).switchMap((observables) {       return observables.length > 0           ? Observable.combineLatestList(observables)           : Observable.just([]);     }) } 

for rxdart 0.23.x

@override       void initState() {         super.initState();         _combineStream = Firestore.instance             .collection('chat')             .orderBy("timestamp", descending: true)             .snapshots()             .map((convert) {           return convert.documents.map((f) {              Stream<Messages> messages = Stream.value(f)                 .map<Messages>((document) => Messages.fromSnapshot(document));              Stream<Users> user = Firestore.instance                 .collection("users")                 .document(f.data['uid'])                 .snapshots()                 .map<Users>((document) => Users.fromSnapshot(document));              return Rx.combineLatest2(                 messages, user, (messages, user) => CombineStream(messages, user));           });         }).switchMap((observables) {           return observables.length > 0               ? Rx.combineLatestList(observables)               : Stream.value([]);         })     } 
like image 129
Cenk YAGMUR Avatar answered Oct 01 '22 22:10

Cenk YAGMUR


I got another version working which seems slightly better than my answer with the two nested builders.

Here I isolated on the data loading in a custom method, using a dedicated Message class to hold the information from a message Document and the optional associated user Document.

class Message {   final message;   final timestamp;   final uid;   final user;   const Message(this.message, this.timestamp, this.uid, this.user); } class ChatList extends StatelessWidget {   Stream<List<Message>> getData() async* {     var messagesStream = Firestore.instance.collection("chat").orderBy("timestamp", descending: true).snapshots();     var messages = List<Message>();     await for (var messagesSnapshot in messagesStream) {       for (var messageDoc in messagesSnapshot.documents) {         var message;         if (messageDoc["uid"] != null) {           var userSnapshot = await Firestore.instance.collection("users").document(messageDoc["uid"]).get();           message = Message(messageDoc["message"], messageDoc["timestamp"], messageDoc["uid"], userSnapshot["name"]);         }         else {           message = Message(messageDoc["message"], messageDoc["timestamp"], "", "");         }         messages.add(message);       }       yield messages;     }   }   @override   Widget build(BuildContext context) {     var streamBuilder = StreamBuilder<List<Message>>(           stream: getData(),           builder: (BuildContext context, AsyncSnapshot<List<Message>> messagesSnapshot) {             if (messagesSnapshot.hasError)               return new Text('Error: ${messagesSnapshot.error}');             switch (messagesSnapshot.connectionState) {               case ConnectionState.waiting: return new Text("Loading...");               default:                 return new ListView(                   children: messagesSnapshot.data.map((Message msg) {                     return new ListTile(                       title: new Text(msg.message),                       subtitle: new Text(DateTime.fromMillisecondsSinceEpoch(msg.timestamp).toString()                                          +"\n"+(msg.user ?? msg.uid)),                     );                   }).toList()                 );             }           }         );         return streamBuilder;   } } 

Compared to the solution with nested builders this code is more readable, mostly because the data handling and the UI builder are better separated. It also only loads the user documents for users that have posted messages. Unfortunately, if the user has posted multiple messages, it will load the document for each message. I could add a cache, but think this code is already a bit long for what it accomplishes.

like image 31
Frank van Puffelen Avatar answered Oct 01 '22 22:10

Frank van Puffelen