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
fieldIn 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?
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.
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([]); }) }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With