Combine two streams based on field within their objects in Flutter - firebase

I have two queries for Firebase. One that returns a list of posts ("PostsModel") and one that returns a list of liked posts for a particular user ("LikedPostsModel"). I want to set an attribute called "liked" within "PostsModel" to true if it's also in the liked posts streams.
Pseudo Code:
/* Returns PostsModel, ex. [PostsModel(id: 1, liked: false), PostsModel(id: 2, liked: false), PostsModel(id: 3, liked: false), ...] */
Stream<List<PostsModel>> getListOfPosts();
/* Returns LikedPostsModel, ex. [LikedPostsModel(postsmodel_id: 2), ...] */
Stream<List<LikedPostsModel>> getListOfLikedPosts();
/* Returns combining PostsModel and LikedPostsModel, ex. [PostsModel(id: 1, liked: false), PostsModel(id: 2, liked: true), PostsModel(id: 3, liked: false), ...] */
Stream<List<LikedPostsModel>> getCombinedPosts() {
var listOfPosts = getListOfPosts();
var listOfLikedPosts = getListOfLikedPosts();
if PostsModel.id within listOfPosts is matches LikedPostsModel.postsmodel_id within listOfLikedPosts, then set PostsModel.liked = true;
return Stream<List<PostsModel>> with some fields "liked" set to true;
}
Actual Code:
My query to retrieve all posts:
Stream<List<PostsModel>> getListOfPostsStream() {
return _firestore
.collection("posts")
.snapshots()
.map((QuerySnapshot<Map<String, dynamic>> query) {
List<PostModel> postsModelList = [];
query.docs.forEach((element) {
postsModelList.add(PostsModel.fromDocumentSnapshot(element));
});
return postsModelList;
});
}
My query to retrieve liked posts:
#override
Stream<List<LikedPostsModel>> getLikedPostsByUser(User user) {
return _firestore
.collection("users")
.doc(user.id)
.collection("liked_posts")
.snapshots()
.map((QuerySnapshot<Map<String, dynamic>> query) {
List<LikedPostsModel> likedModelList = [];
query.docs.forEach((element) {
likedModelList.add(LikedPostsModel.fromDocumentSnapshot(element));
});
return likedModelList;
});
}
PostsModel
class PostsModel {
late String id;
late String bodyText;
bool liked = false;
PostsModel(
this.id,
this.bodyText,
);
...
}
LikedPostsModel
class LikedPostsModel {
late String id;
late String postsmodel_id;
bool liked;
LikedPostsModel(
this.id,
this.postsmodel_id,
this.liked
);
...
}
Any help is appreciated. Thanks!

To achieve it you should use the combineLatest2 method of the RxDart package.
I used it when developing a chat project. You know, at the conversations page of WhatsApp lists all chats and shows us the talker and the last message, message time, and seen information. My problem was my talker detail values and chat metadatas were in separate collections and I had to show the profile image and name of a talker and the last message, last message time and isSeen of the chat at the same time.
Here is an example from my chat project;
Stream<List<Chats>> getChats() {
final _userId = FirebaseAuth.instance.currentUser?.uid;
return _firestore
.collection('Chats')
.where('members', arrayContains: _userId)
.orderBy("lastMessageDate", descending: true)
.snapshots()
.map((convert) {
return convert.docs.map((f) {
String? talkerId;
var members = f.data()['members'];
if (members.length == 2) {
talkerId = members[0] != _userId ? members[0] : members[1];
}
Stream<ChatMetadata> chatStream = Stream.value(f).map<ChatMetadata>(
(document) => ChatMetadata.fromMap(
f.reference.path,
document.data(),
),
);
Stream<Talker> talkerStream = _firestore
.collection("Users")
.where('uid', isEqualTo: talkerId)
.snapshots()
.map<Talker>(
(querySnapshot) => querySnapshot.docs
.map((e) => Talker.fromMap(
e.data(),
))
.first,
);
return Rx.combineLatest2(
chatStream,
talkerStream,
(ChatMetadata chatMetadata, Talker talker) =>
Chats.combine(chatMetadata, talker),
);
});
}).switchMap((observables) {
return observables.isNotEmpty
? rx.Rx.combineLatestList(observables)
: Stream.value([]);
});
}
As you can see the getChats() method returns a Stream<List<Chats>>. The Chats model is a class that combines two other classes like ChatMetadata and Talker. Here is my Chats model;
import '/model/talker.dart';
import '/model/chat_metadata.dart';
class Chats {
String? docId;
String? talkerId;
String? talkerUsername;
String? talkerUnvan;
String? talkerProfileImage;
String? lastMessage;
DateTime? dateTime;
String? lastMessageOwner;
bool? isSeen;
List? members;
Chats({
this.docId,
this.talkerId,
this.talkerUsername,
this.talkerUnvan,
this.isSeen,
this.talkerProfileImage,
this.lastMessage,
this.dateTime,
this.lastMessageOwner,
this.members,
});
factory Chats.combine(ChatMetadata chat, Talker talker) {
return Chats(
docId: chat.docId,
talkerId: talker.id,
talkerUsername: talker.username,
talkerUnvan: talker.unvan,
isSeen: chat.isSeen,
talkerProfileImage: talker.photoUrl,
lastMessage: chat.lastMessage,
dateTime: chat.lastMessageDate,
lastMessageOwner: chat.lastMessageOwner,
members: chat.members,
);
}
}
You have to pay attention to the Rx.combineLatest2 method and switchMap method at the end of the code.
You can research all of them but in summary, the combineLatest2() merges the given Streams into a single Stream sequence by using the [combiner] function whenever any of the stream sequences emits an item. Then switchMap() converts each emitted item into a Stream using the given mapper function. Because the getChats() method has to returns the Stream<List<Chats>>.

You can store your likes in a Map rather than a List.
Stream<Map<String, bool>> getLikedPostsByUser(User user) {
return _firestore
.collection("users")
.doc(user.id)
.collection("liked_posts")
.snapshots()
.map((QuerySnapshot<Map<String, dynamic>> query) {
final likedPostsMap = HashMap<String, bool>();
query.docs.forEach((element) {
final model = LikedPostsModel.fromDocumentSnapshot(element);
likedPostsMap[model.postmodel_id] = model.liked;
});
return likedPostsMap;
});
}
Then use the resulting Map for a constant time lookup when populating your list of posts.
Stream<List<PostsModel>> getListOfPostsStream(Map<String, bool> likedPosts) {
return _firestore
.collection("posts")
.snapshots()
.map((QuerySnapshot<Map<String, dynamic>> query) {
List<PostModel> postsModelList = [];
query.docs.forEach((element) {
final model = PostsModel.fromDocumentSnapshot(element);
model.liked = likedPosts[model.id] ?? false;
postsModelList.add(model);
});
return postsModelList;
});
}
To improve performance, likes can be persisted locally so they don't have to be fetched each time your app is opened. Add a timestamp to your likes document to limit your query to changes since the last time you fetched.

Related

How to display all the names in database firestorefirebase flutter

In my app I have a model which consists of the store name and store image and looks like this:
class StoreModel
{
String? imageofStore;
String? storeName;
StoreModel({ this.imageofStore, this.storeName});
//data from server
factory StoreModel.fromMap(map)
{
return StoreModel(
imageofStore: map['imageofStore'],
storeName: map['storeName'],
);
}
// data to server
Map<String, dynamic> toMap(){
return{
'imageofStore': imageofStore,
'storeName': storeName,
};
}
}
and my database for stores looks like this:
to call the store name I use initstate and setState as such:
class addStore extends StatefulWidget {
const addStore({Key? key}) : super(key: key);
#override
_addStoreState createState() => _addStoreState();
}
class _addStoreState extends State<addStore> {
User ? user = FirebaseAuth.instance.currentUser;
StoreModel storebox = StoreModel();
#override
void initState()
{
super.initState();
FirebaseFirestore.instance
.collection("stores")
.doc("XQjbm665g2a2xAiiydjr")
.get()
.then((value){
this.storebox = StoreModel.fromMap(value.data());
setState(() {
});
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Text("${storebox.storeName}"),
);
}
}
With this, I get the store name of the store with id XQjbm665g2a2xAiiydjr displaying but the thing is I want to get the name of all the stores. I know I need to change the .doc() but im not sure as to what I am to put in it that will start displaying all the names. Can someone please help?
By providing a document id, what you're getting is a DocumentSnapshot which is the data of a particular Document, but when you remove it, you get QuerySnapshot which is a list of the data of all the documents. So, to read all, you change your code as:
List<StoreModel> storesList = [];
FirebaseFirestore.instance
.collection("stores")
.get()
.then((value){
//Now, this value is of type QuerySnapshot unlike earlier.
if (value != null && value.docs.isNotEmpty) {
//If it comes here means the collection is not empty.
//Because this value is a list of DocumentSnapshot, We've to map it to extract documents.
//After mapping, returning it as a list and assigning it to storesList
storesList = value.docs.map((doc) => StoreModel.fromMap(doc.data())).toList();
setState(() {
});
} else {
//If it comes here means there are no documents in your collection.
//Notify User there's no data.
}
});
This code will get all the documents your collection have, but, you can limit or filter using limit or where respectively, just place .limit(int) or .where() before .get().
for model try to convert to json or you can use your either way this is just for example model
import 'dart:convert';
List<StoreModel> storeModelFromJson(String str) => List<StoreModel>.from(json.decode(str).map((x) => StoreModel.fromJson(x)));
String storeModelToJson(List<StoreModel> data) => json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
class StoreModel {
StoreModel({
this.imageofStore,
this.storeName,
});
final String? imageofStore;
final String? storeName;
factory StoreModel.fromJson(Map<String, dynamic> json) => StoreModel(
imageofStore: json["imageofStore"] == null ? null : json["imageofStore"]!,
storeName: json["storeName"] == null ? null : json["storeName"]!,
);
Map<String, dynamic> toJson() => {
"imageofStore": imageofStore == null ? null : imageofStore!,
"storeName": storeName == null ? null : storeName!,
};
}
try this get all list
static Future<List<StoreModel>> getStorelist() async {
List<dynamic> list = [];
await FirebaseFirestore.instance
.collection("stores")
.get()
.then((value){
for(var x in value.docs){
final Map<String,dynamic> toMap = x.data() as Map<String,dynamic>;
/// Try to print all data first to see if fetching use log to view on terminal
log(toMap.toString());
list.add(toMap);
}
});
return list.map((e)= > StoreModel.fromJson(e)).toList();
}
as in my case to get only 1 data
static getSingleName(String? uid) async {
final result =
await FirebaseFirestore.instance
.collection("stores").doc(uid).get();
final response = result.data() as Map<String, dynamic>;
return response['storeName'].toString();
}

Flutter Firebase async query not retrieving data inside a stream function

I am trying to query a User from firebase within another query but for some reason but I can't get the code to work
The function the wont run is await usersRef.doc(uid).get(); and can be found here:
static getUserData(String uid) async {
return await usersRef.doc(uid).get();
}
static DirectMessageListModel getDocData(QueryDocumentSnapshot qdoc, String uid) {
Userdata postUser = Userdata.fromDoc(getUserData(uid));
return DirectMessageListModel.fromDoc(qdoc, postUser);
}
static DirectMessageListModel fromDoc(QueryDocumentSnapshot doc, Userdata altUser) {
return DirectMessageListModel(
doc['chatId'],
doc['lastMsgContent'],
doc['lastMsgType'],
altUser
);
}
parent function:
Stream<List<DirectMessageListModel>> getMeassageList(){
var snaps = FirebaseFirestore.instance.collection('directMessages').where('users', arrayContains: userdata!.uid).snapshots();
List<String> usersListElement = [];
return snaps.map((event) { return event.docs.map((e) {
usersListElement = [e.get('users')[0], e.get('users')[1]];
usersListElement.remove(userdata!.uid);
return DirectMessageListModel.getDocData(e, usersListElement.first);
}).toList();
});
}
You forgot to wait for the future getUserData(uid) to complete.
Try this:
static Future<DocumentSnapshot<Object>> getUserData(String uid) async {
return await usersRef.doc(uid).get();
}
static DirectMessageListModel getDocData(
QueryDocumentSnapshot qdoc,
String uid,
) async {
Userdata postUser = Userdata.fromDoc(await getUserData(uid)); // await here
return DirectMessageListModel.fromDoc(qdoc, postUser);
}
..
// parent function.
// Also wait for the future in the parent function.
// UPDATE BELOW! Define the parent function like this:
Stream<List<Future<DirectMessageListModel>>> getMeassageList() {
var snaps = FirebaseFirestore.instance
.collection('directMessages')
.where('users', arrayContains: userdata!.uid)
.snapshots();
List<String> usersListElement = [];
return snaps.map((event) {
return event.docs.map((e) async {
usersListElement = [e.get('users')[0], e.get('users')[1]];
usersListElement.remove(userdata!.uid);
return await DirectMessageListModel.getDocData(e, usersListElement.first);
}).toList();
});
}
NB: You are fetching user data (either sender/receiver) for each message in directMessages collection. It might be better to store just sender/receiver name in directMessages collection and simply display that. Then if the user clicks on a message, you can then fetch the full sender/receiver data.

Fllutter/Firestore - realtime listening to collections and subcollections

I have a firebase collection named 'reviews' with a sub-collection 'clients'.
I am looking to fetch all reviews in realtime with their owners from Firebase Firestore but I got a bit lost when it came to correctly mapping the data and returning the listener's result.
This is 'reviews' model:
class Review {
final String reviewTitle;
final String reviewContent;
final String reviewCategory;
final String reviewTimestamp;
final int reviewVotesCount;
final Client client;
Review(
{this.reviewTitle,
this.reviewContent,
this.reviewCategory,
this.reviewTimestamp,
this.reviewVotesCount,
this.client});
}
This is the Service class:
class ReviewService {
var currentUser = FirebaseAuth.instance.currentUser;
var firestoreInstance = FirebaseFirestore.instance;
List<Review> fetchAllThreads() {
Review review;
Client client;
List<Thread> mReviewsList = new List<Review>();
firestoreInstance.collection('reviews').snapshots().listen((result) {
result.docs.forEach((result) {
firestoreInstance
.collection('reviews')
.doc(result.id)
.collection('clients')
.get()
.then((result) {/*here I get the result.data()*/});
});
});
}
Question after I get result.data() how can I map it to my model so I can add the result object to mReviewsList and then return mReviewsList ?
You can add a factory constructor in your Review class to create it from a Map and same applies for Client.
factory Review.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return Review(
reviewTitle: map['reviewTitle'],
reviewContent: map['reviewContent'],
reviewCategory: map['reviewCategory'],
reviewTimestamp: map['reviewTimestamp'],
reviewVotesCount: map['reviewVotesCount'],
client: Client.fromMap(map['client']),
);
}
If you're using VS Code, 'Dart Data Class Generator' extension can be handy there, and also there are multiple code generation packages in pub.dev for serialization and deserialization
Now in the place of your comment, you can do this:
mReviewsList.add(Review.fromMap(result.data()));
Update:
Based on Doug's comment, if you like to map your the data to your model and return a stream, you can create a helper function as follow:
Stream<List<T>> collectionStream<T>({
#required String path,
#required T builder(Map<String, dynamic> data),
}) {
final reference = FirebaseFirestore.instance.collection(path);
final snapshots = reference.snapshots();
return snapshots
.map((snapshot) => snapshot.docs.map((snapshot) => builder(snapshot.data())).toList());
}
To use it, simply call it as follow:
final stream = collectionStream<Review>(path: "reviews", builder: (data) => Review.fromMap(data));
if you like to fetch the data only once, you can create a helper function for that too:
Future<List<T>> getDocuments<T>({
String path,
#required T builder(Map<String, dynamic> data),
}) async {
final reference = FirebaseFirestore.instance.collection(path);
final snapshots = await reference.get();
final docs = snapshots.docs.map((doc) => builder(doc.data())).toList();
return docs;
}
and call it the same way:
final reviews = getDocuments<Review>(path: "reviews", builder: (data) => Review.fromMap(data));

Flutter FutureBuilder returns nothing - Cloud Firestore and Deserialization

I have the following problem. I'm trying to receive data from my Firestore collection called tournaments. I'm querying the database from within my DatabaseService class. That looks like the following:
class Collection<T> {
final Firestore _db = Firestore.instance;
final String path;
CollectionReference ref;
Collection({this.path}) {
ref = _db.collection(path);
}
Future<List<Tournament>> getData() async {
var snapshots = await ref.getDocuments();
return snapshots.documents
.map((doc) => Global.models[Tournament](doc.data))
.toList();
}
}
The widget implements a FutureBuilder
Widget build(BuildContext context) {
return FutureBuilder(
future: Global.tournamentRef.getData(),
builder: (BuildContext context, AsyncSnapshot snap) {
if (snap.connectionState == ConnectionState.done) {
List<Tournament> tournaments = snap.data;
...
I want to deserialize the firestore data into a Tournament object. I defined the Tournament class as this:
class Tournament {
String id;
String name;
String mode;
String owner;
int size;
Tournament({this.id, this.name, this.mode, this.owner, this.size});
factory Tournament.fromMap(Map data) {
return Tournament(
id: data["id"] ?? '',
mode: data["mode"] ?? '',
name: data["name"] ?? "group",
owner: data["owner"] ?? "",
size: data["size"] ?? 6);
}
}
The last class that is important is the globals.dart
class Global {
static final Map models = {Tournament: (data) => Tournament.fromMap(data)};
static final Collection<Tournament> tournamentRef =
Collection<Tournament>(path: "tournaments");
}
It simply specifies the collection path. I want the data to be deserialized but I don't have a clue why it isn't returning anything. I've tried querying the database in a simple old-fashioned way like
Future<dynamic> retrieveData() async {
var querySnap = await ref.getDocuments();
var list = querySnap.documents.map((snap) => snap.data);
return list;
}
That worked perfectly fine, however it doesn't help with deserialization at all. I think i missed something at some point and as you may notice I'm still a Flutter/dart beginner and some of these discussed topics are a bit too complicated to me.
I appreciate any help.
Thank you

Get array of ID from Firebase (DART/Flutter)

Is there any way to fetch data from Firebase with an array of documents.
Now I do this with loop. It works but maybe there is some more efficient way to fetch data? Members list can contain up to 10k users so create 10k requests seems wrong.
Many thanks!
ref = db.collection('users');
Future<List<User>> fetchAllMembers({List<dynamic> members}) async {
List<User> results = [];
for (String userID in members) {
await ref.document(userID).get().then((result) {
results.add(User.fromMap(result.data, result.documentID));
});
}
return results;
}
SOLVED
So simple :). Working example below. Many thanks!
final Firestore db = Firestore.instance;
ref = db.collection('users');
List<dynamic> membersIDS = ['DSGSGSG', 'IBIOSCP3S', 'ASDUASDGU'];
/// Fetch members list
Future<List<User>> fetchAllMembers({List<dynamic> membersIDS}) async {
/// With whereIn
var result = await ref.where('uid', whereIn: members).getDocuments();
var documents = result.documents.map((doc) => User.fromMap(doc.data, doc.documentID)).toList();
return documents;
/// With loop
// List<User> results = [];
// for (String userID in members) {
// await ref.document(userID).get().then((result) {
// results.add(User.fromMap(result.data, result.documentID));
// });
// }
// return results;
}
Query documents where the given field matches any of the comparison values
userRef.where('id', 'in',
[['DSGSGSG', 'IBIOSCP3S', 'ASDUASDGU']]);

Resources