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.
I want two streams to be combined as one result and then return it from my function. I have two streams coming from firebase. So I have to merge data1 into data2, something like concatenation of both the streams and then return. What i have acheived so for is:
Future<Stream<QuerySnapshot>> getMsgs(userId, toId) async {
var data1 = await _fireStore
.collection(_collection)
.where('messageFrom', isEqualTo: userId).where('messageTo', isEqualTo: toId)
.orderBy('createdOn', descending: true)
.snapshots();
var data2 = await _fireStore
.collection(_collection)
.where('messageFrom', isEqualTo: toId).where('messageTo', isEqualTo: userId)
.orderBy('createdOn', descending: true)
.snapshots();
// I have tried this, but this will be the list
return [data1, data2];
// I have tried this, but didn't worked
return StreamZip([data1, data2]);
}
Any help would be appreciated.
In Dart/Flutter and learning Firebase Firestore... I'm using the following method to test before creating UI:
_testFireStore() async {
var result = Firestore.instance
.collection('users')
.where('uid', isEqualTo: 'IvBEiD990Vh0D9t24l2GCCdsrAf1')
.snapshots();
await for (var snapshot in result) {
for (var user in snapshot.documents) {
print('main.DEBUG: ' + user.data.toString());
}
}
}
It works as expected -- the print statement is executed initially, but also subsequently in real-time every time any field is updated in the document in the Firestore database.
How can this code be changed such that the snapshot is only retrieved once -- not "subscribed/listened" to... and thus we don't waste bandwidth on unwanted/unneeded data and the print statement is only executed once?
Firestore.instance.collection(...).where(...) returns a Query object. It has a method called getDocuments() that executes the query and gives you a Future with a single set of results.
var query = Firestore.instance
.collection('users')
.where('uid', isEqualTo: 'IvBEiD990Vh0D9t24l2GCCdsrAf1');
query.getDocuments().then((QuerySnapshot snapshot) {
// handle the results here
})
Or use await to get the QuerySnapshot, since getDocumets() returns a Future.
Use getDocuments(), to retrieve all the documents once:
_testFireStore() async {
var result = await Firestore.instance
.collection('users')
.where('uid', isEqualTo: 'IvBEiD990Vh0D9t24l2GCCdsrAf1')
.getDocuments();
print(result.documents.toString());
}
I'm implementing a Todo Application in Flutter. I need to merge a double query in client, in order to perform an OR request in Firestore.
One hand, I have the following code that performs the double queries.
Future<Stream> combineStreams() async {
Stream stream1 = todoCollection
.where("owners", arrayContains: userId)
.snapshots()
.map((snapshot) {
return snapshot.documents
.map((doc) => Todo.fromEntity(TodoEntity.fromSnapshot(doc)))
.toList();
});
Stream stream2 = todoCollection
.where("contributors", arrayContains: userId)
.snapshots()
.map((snapshot) {
return snapshot.documents
.map((doc) => Todo.fromEntity(TodoEntity.fromSnapshot(doc)))
.toList();
});
return StreamZip(([stream1, stream2])).asBroadcastStream();
}
And other hand, I have the following code that will perform the update of view with the Bloc pattern.
Stream<TodosState> _mapLoadTodosToState(LoadTodos event) async* {
_todosSubscription?.cancel();
var res = await _todosRepository.todos(event.userId);
_todosSubscription = res.listen(
(todos) {
dispatch(
TodosUpdated(todos));
},
);
}
I have the following error.
flutter: Instance of '_AsBroadcastStream<List<List<Todo>>>'
[VERBOSE-2:ui_dart_state.cc(148)] Unhandled Exception: type 'List<List<Todo>>' is not a subtype of type 'List<Todo>'
I tried to look for more infos with the debugger and it turned out that my StreamZip source contains the 2 stream separately.
For the moment I can get one stream at a time.
I don't know how to proceed in order to get the 2 streams and display them.
You're doing a map of a map, which returns a List of another List.
You should zip the QuerySnapshot streams and do the mapping after creating the subscription, and then you can create a new Stream<List<Todo>> from it.
///private method to zip QuerySnapshot streams
Stream<List<QuerySnapshot>> _combineStreams() async {
Stream stream1 = todoCollection
.where("owners", arrayContains: userId)
.snapshots()
});
Stream stream2 = todoCollection
.where("contributors", arrayContains: userId)
.snapshots()
});
return StreamZip(([stream1, stream2])).asBroadcastStream();
}
///exposed method to be consumed by repository
Stream<List<Todo>> todosStream() {
var controller = StreamController<List<Todo>>();
_combineStreams().listen((snapshots) {
List<DocumentSnapshot> documents;
snapshots.forEach((snapshot) {
documents.addAll(snapshot.documents);
});
final todos = documents.map((document) {
return Todo.fromEntity(TodoEntity.fromSnapshot(doc));
}).toList();
controller.add(todos);
});
return controller.stream;
}
I am trying to make 2 queries to Firestore and merge the results into one in order to simulate a Firestore OR query.
I segmented my code according to the bloc pattern.
///private method to zip QuerySnapshot streams
Stream<List<QuerySnapshot>> _combineStreams(String userId) {
var stream1 = todosCollection
.where("owners", arrayContains: userId)
.snapshots();
var stream2 = todosCollection
.where("contributors", arrayContains: userId)
.snapshots();
return StreamZip(([stream1, stream2])).asBroadcastStream();
}
///exposed method to be consumed by repository
Stream<List<Todo>> todos(String userId) {
var controller = StreamController<List<Todo>>();
_combineStreams(userId).listen((snapshots) {
List<DocumentSnapshot> documents = List<DocumentSnapshot>();
snapshots.forEach((snapshot) {
documents.addAll(snapshot.documents);
});
final todos = documents.map((doc) {
return Todo.fromEntity(TodoEntity.fromSnapshot(doc));
}).toList();
controller.add(todos);
});
return controller.stream;
}
In my bloc I have the following code that should update my view accordingly my database state but it's not working. The database insertion work but the view doesn't refresh and I don't know why.
_gardensSubscription?.cancel();
_gardensSubscription = _dataRepository.gardens(event.userId).listen(
(gardens) {
dispatch(
GardensUpdated(gardens),
);
},
);
I am not very confortable with Stream and particularly with StreamController process. Is it possible to do this task more easily?
Time to use the great powers of RxDart: https://pub.dev/packages/rxdart
You can't do all types of streams transformations with this lib.
For example, you can use the merge operators to achieve exactly what you want