Getting Bool State from Firestore in Dart - flutter

I am trying to get a bool value from Firestore when the app is being initialized: it returns True if it is "like" and False if it is not "like". Every time a user likes/unlikes a post, a database (called userFavorites) is being created or update on Firestore. The userFavorite database is composed of: document (user's ID), collection ('posts'), document (post's ID), collection (isLiked: true OR isLiked: false). So when initializing the app, I'm trying to get access to this True/False for each of the posts that are being displayed on the UI (if the user has never liked or unliked the post, the value for this bool will automatically be False).
I would really appreciate if you can give me feedback/corrections on the code I use to get the True/False bool value from Firestore, because even though I am not getting any errors, the bool value on my IU is Null, and I don't know whether I made an error in this part of my code or in another part.
Here is the code I used:
class HomeFeed extends StatelessWidget {
final user = FirebaseAuth.instance.currentUser;
ValueKey valueKey;
Future<DocumentSnapshot> getDocumentSnapshotForCurrentUserLikes(String userId, String documentId) async {
final String userId = user.uid;
final String documentId = valueKey.value;
return await FirebaseFirestore.instance.collection('userFavorites').doc(userId).collection('posts').doc(documentId).get();
}
bool getCurrentUserLikesValue(DocumentSnapshot documentSnapshotForCurrentUserLikes) {
return documentSnapshotForCurrentUserLikes.data()['isLiked'];
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('post').doc('Post in Feed').collection('posts').orderBy('createdAt', descending: true,).snapshots(),
builder: (ctx, AsyncSnapshot<QuerySnapshot> postSnapshot) {
if (postSnapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
final postDocs = postSnapshot.data.docs;
return ListView.builder(
reverse: false,
itemCount: postDocs.length,
itemBuilder: (ctx, index) {
ValueKey valueKey = ValueKey(postDocs[index].id);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: PostContainer(
user.uid,
postDocs[index].data()['likes'],
getCurrentUserLikesValue(postDocs[index]) == null ? false : getCurrentUserLikesValue(postDocs[index]),
key: valueKey,
),
),
);
},
);
},
);
}
}
Thank you for the input! Do you mean using something like this:
IconButton(
icon: Icon(
isLiked == true ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
size: 25.0,
),
onPressed: () async{
DocumentReference docRef = FirebaseFirestore.instance.collection('userFavorites').doc(this.widget.userId);
DocumentSnapshot doc = await docRef.get();
List userLikedPosts = doc.data['userLikedPosts'];
if(userLikedPosts.contains(documentId)==true) {
docRef.update({'userLikedPosts' : FieldValue.arrayRemove([documentId])});
} else {
docRef.update({'userLikedPosts' : FieldValue.arrayUnion([documentId])});
}
If this is kind of code you are referring to, how can I use it with "set", instead of "get" (because if I use "get", the user reference would have to be created in Firebase beforehand for every user, which would be inefficient)? Also for doc.data['userLikedPosts'], I get an error for ['userLikedPosts'], which says "The operator '[]' isn't defined for the type 'Map<String, dynamic> Function()'. Try defining the operator '[]'." How would you solve this? Thanks a lot for the help!
Hello! I have been researching and working on it for a while, and the problem that I am having is that I am not able to get and display the T/F bool value from the Firestore database into the UI each time a post is initialized (or seen on the screen). What code could I use to do that? I would really appreciate your help here. Thanks!

You're making your life much harder than it should be:
Consider a db structure like this:
userFavorite (collection)
|---{userId} (document)
|--- liked_posts (Array) [array of post Ids the posts the user liked]
|--- ['postA', 'postB', 'post3131',...]
By doing it this way, for each postID, you can just check if it exists in that liked_posts array. This a cleaner way to do things. You don't need extra document and collection levels.
When the user clicks a "like" on a post, you can use ArrayUnion to add that postId to the liked_posts field of that user.
Update:
Sorry, I can't help you with Flutter code. All I can say is this:
? did the user like the post? If so, you can update the userLikedPosts (Array) field WITHOUT reading it first. With ArrayUnion, if the postId is within that array, it won't be added, if it's not there it will be added, the other elements in the Array will not be changed.
? did the user dislike the post? If so, you can update userLikedPosts (Array) field WITHOUT reading it first. With ArrayRemove, if the postId is within that array, it will be removed, if it's not there then nothing happens, the other elements in the Array will not be changed.
In your place, I would not use update():
docRef.update({'userLikedPosts' : FieldValue.arrayRemove([documentId])});
Instead, I would use set() with {merge:true}:
docRef.set( {'userLikedPosts' : FieldValue.arrayRemove([documentId])}, {merge:true} );
ArrayUnion/ArrayRemove works flawlessly with set() and won't rewrite the array. Also, if the document doesn't exist, then it will be created automatically.
Sorry I can't help you with actual Flutter code, but my main point is that you do not need to read the document containing the userLikedPosts Array when responding to LIKE/DISLIKE user actions. You only need to read it when displaying whether or not the post is liked by the user, and only on subsequent post page visits. When the user presses like, you can respond in the UI immediately and the logic above to update the db with set/merge:true and ArrayUnion.

Related

Flutter getx: How to send data between pages from FirestoreQueryBuilder

I'd Like to get data in home screen of my flutter app, where I have list of OfferCards, these are generated from firestore via FirestoreQueryBuilder in my homeView like this
FirestoreQueryBuilder<OfferData>(
pageSize: 10,
query: FirebaseFirestore.instance
.collection('Offers')
.orderBy('CreatedAt', descending: true)
.withConverter<OfferData>(
fromFirestore: ((snapshot, options) =>
OfferData.fromJson(snapshot.data()!)),
toFirestore: (value, options) => value.toJson()),
builder: (context, snapshot, _) {
if (snapshot.isFetching) {
return const Center(
child: CircularProgressIndicator(color: Colors.greenAccent),
);
} else if (snapshot.hasError) {
return const Center(
child: Text('Server error'),
);
} else if (snapshot.docs.isEmpty) {
return const Center(
child: Text('No offers'),
);
} else {
return ListView.builder(
itemBuilder: (context, index) {
final hasReachEnd = snapshot.hasMore &&
index + 1 == snapshot.docs.length &&
!snapshot.isFetchingMore;
if (hasReachEnd) {
snapshot.fetchMore();
}
final post = snapshot.docs[index].data();
homeController.offers[index] = post;
return OfferCardView();
},
itemCount: snapshot.docs.length);
}
},
)
As on the end of this example, inside HomeController I have Map of int and UserData, which is filled with all offers. Each offerCardView has Get.find to HomeController to have access to this map. And here's my question, how do I determine inside of OfferCardView and later in OfferView(after tapping on given OfferCardView) which entry from map is being clicked on/view filled with. I don't know how to acomplish this, I'm aware that using Map here is bad decision, but I don't have clue how this should be done
The better practice is passing each document data with its index to the OfferView() constructor, so for every OfferCardView() that will be clicked, OfferView() will be opened with that data.
This ensures that your data will not rely on the GetxController availability, since depending on GetxController to exchange data like this could simply break.
For example :
While your app is growing and somewhere the controller is deleted either by Getx or manually using Get.delete() ( or you needed to call multiple controllers with different tags ), then Get.find() will not find that controller or mistake it, this leads to unexpected behaviors, which will put you in a hard time to find out what went wrong in your project.
Using GetPage, if you're required to assign the model data property, you could make a placeholder model for that data by default where we would say like :
There is no data so we showed you that placeholder alternative data page with this data.
This gives the user at least an overview of what's happening, not just a direct crash for the app.
I would say it's a good practice for the user experience.
You can share variables from other controllers onto another controller by using GetX Dependency Injection
On binding , add the controller you want to add as a dependency
Get.lazyPut<OfferCardsController>(() => OfferCardsController());
then in the controller
var offerCardsController = Get.find<OfferCardsController>();
you can now access variables from the OfferCardsController onOfferController
e.g
offerCardsController.variableFromCardsController;

Flutter: Weird listview/stream builder behavior

I have a home_page.dart file whose body has a StreamBuilder it recieves the stream from my getChats() function. In the StreamBuilder's builder function I sort the all the documents after storing them in a list named docsList based on the lastMessageTime field so that the document with the latest message is at the top (first).
As the ListView is using the docsList for building the cards it is expected that the document which has the most resent message should be displayed first. Which only happens when the list is build for the first time. After that if I send a new message to the chat which is not at the top this happens:
Initially:
When I send a message which the text 'test' to the chat "Heah Roger" this is how the list gets updated:
As it can be seen the Time on the right and the subtext changes for the first tile but the image and name didn't (same for second tile). Even though the documents are updated in the docsList and are sorted in the desired manner (I printed it to check it). Somehow the photo and the name alone are not being updated in the UI alone.
Note: The correct fields are updated in the firestore. Also if I restart the app after killing it. It shows the desired result:
getChats()
Stream<QuerySnapshot<Map<String, dynamic>>> getChats(User currentUser) {
return FirebaseFirestore.instance
.collection('chats')
.where('users', arrayContains: currentUser.id)
.snapshots();
}
home_page.dart
body: StreamBuilder(
stream: RepositoryProvider.of<FirestoreRepository>(context).getChats(BlocProvider.of<AuthenticationBloc>(context).state.user),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData && snapshot.data.docs.length > 0) {
List docsList = snapshot.data.docs;
docsList.sort((a, b) => b.data()['lastMessageTime'].compareTo(a.data()['lastMessageTime']));
return ListView.builder(
itemCount: docsList.length,
itemBuilder: (context, index) {
return SingleChatCard(chatRoom: ChatRoomModel.fromMap(docsList[index].data()));
},
);
} else {
return ...
}
},
),
Can anyone help me figure out the underlying problem that is causing this weird behavior?
Looks like an key issue to me,
When you're using your custom Widget to render in a listview, with some complicate data flow,
Flutter react to these changes one level at a time:
You can refer: https://www.youtube.com/watch?v=kn0EOS-ZiIc
In your example, you can do something like this:
return SingleChatCard(
key: ValueKey(index),
chatRoom: ChatRoomModel.fromMap(docsList[index].data()));
},

Flutter + Firestore chat .. Listview rebuilds all items

I'm trying to add chat functionality to my app (kind of similar to WhatsApp functionalities), using flutter and Firestore. The main structure on Firestore is to have 2 collections (I want the unread message count as well):
users: and each user will have a subcollection "chats" that will include all CHATS_IDs. This will be the main place to build home chat page (shows a history list of all chats) by getting the user chat list.
chats: a list of all chats and each chat document has a subcollection of messages.
My main issue is in building the home page (where a list of all user previous chats should appear). I get/subscribe the user chat subcollection, and for each chat ID listed in there I also subscribe for the chat itself in the chat collection (using the ID).
Here are screenshots of it in principle:
users collection:
chats coleection:
and here is the main screen of interest (principle from whatsapp screen):
What I'm doing is that I retrieve user's chat subcollection (and register a listener to it using StreamBuilder), and also for number of unread messages/last message and last message time, I subscribe to listen for each of these chats (and want to use each user last message time, status and his last presence in that chat doc to calculate the unread count) .
The problem is that Listview.builder rebuilds all items (initially and on scroll) instead of just the viewed ones. here is my code:
Stream<QuerySnapshot> getCurrentUserChats(userId) {
return FirebaseFirestore.instance
.collection(AppConstants.USERS_COLLECTION)
.doc('$userId')
.collection(AppConstants.USER_CHATS_SUBCOLLECTION)
.orderBy('lastMsgTS', descending: true)
.snapshots()
.distinct();
}
Widget getRecentChats(userId) {
return StreamBuilder<QuerySnapshot>(
stream: getCurrentUserChats(userId),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.docs.isNotEmpty) {
print('snapshot of user chats subcoll has changed');
List<QueryDocumentSnapshot> retrievedDocs = snapshot.data.docs;
return Container(
height: 400,
child: ListView.builder(
//childrenDelegate: SliverChildBuilderDelegate(
itemCount: snapshot.data.size,
itemBuilder: (context, index) {
String chatId = retrievedDocs[index].id;
print('building index: $index, chatId: $chatId');
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection(AppConstants.CHATS_COLLECTION)
.doc('$chatId')
.snapshots()
.distinct(),
builder:
(context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasData) {
print('${snapshot.data?.id}, isExist: ${snapshot.data?.exists}');
if (snapshot.data.exists) {
return KeyProxy(
key: ValueKey(chatId),
child: ListTile(
leading: CircleAvatar(
child: Container(
//to be replaced with user image
color: Colors.red,
),
),
title: Text('$chatId'),
subtitle: Text(
"Last Message received on: ${DateTimeUtils.getDateViewFromDT(snapshot.data.data()['ts']?.toDate())}"),
),
);
}
}
return SizedBox.shrink();
},
);
},
/*childCount: snapshot.data.size,
findChildIndexCallback: (Key key) {
print('calling findChildIndexCallback');
final ValueKey valKey = key;
final String docId = valKey.value;
int idx = retrievedDocs.indexOf(retrievedDocs
.where((element) => element.id == docId)
.toList()[0]);
print('docId: $docId, idx: $idx');
return idx;
}*/
),
);
}
return Center(child: UIWidgetUtils.loader());
});
}
After searching, I found these related suggestions (but both didn't work):
A github issue suggested thesince the stream is reordarable (github: [https://github.com/flutter/flutter/issues/58917]), but even with using ListView.custom with a delegate and a findChildIndexCallback, the same problem remained.
to use distinct.
But removing the inner streambuilder and just returning the tiles without a subscription, makes the ListView.builder work as expected (only builds the viewed ones). So my questions are:
Why having nested stream builders causing all items to be rebuil.
is there a better structure to implement the above features (all chats with unread count and last message/time in real-time). Especially that I haven't added lazy loading yet. And also with this design, I have to update multiple documents for each message (in chats collection, and each user's subcollection).
Your help will be much appreciated (I have checked some other SO threads and medium articles, but couldn't find one that combines these features in one place with and preferably with optimized design for scalability/price using Firestore and Flutter).
I think that You can do this:
Widget build(ctx) {
return ListView.builder(
itemCount: snapshot.data.size,
itemBuilder: (index, ctx) =>_catche[index],
)
}
and for _catche:
List<Widget> _catche = [/*...*/];
// initialize on load

Function invoke in Flutter to get attributes from Firebase

I want to define and invoke a function in Flutter to get required values from Firebase.
In the below code I have defined getCourseDetails() function and invoking it in the container by passing a parameter to get the value.
The Course_Details collection has many documents with course_id's which has attribute with name (course_name, subtitle). I use these values to build a listview cards in next steps.
I am able to get the values from the function using async await, but for some reason the values keeps on updating and never stops. It kind of goes to loop and keeps on running. I added print statements to check and it keeps on running and printing.
Please let me know what wrong I am doing here or how to define function here to avoid the issue. Thanks
class _CourseProgressListState extends State<CourseProgressList> {
String course_name, subtitle;
getCourseDetails(course_id_pass) async {
DocumentSnapshot document = await Firestore.instance.collection('Course_Details').document(course_id_pass).get();
setState(() {
course_name = document.data['course_name'];
subtitle = document.data['subtitle'];
});
}
#override
Widget build(BuildContext context) {
return Container(
child: StreamBuilder(
stream: Firestore.instance.collection('Enrolling_Courses').where('student_id', isEqualTo: widget.id).snapshots(),
builder: (context, snapshot) {
if (snapshot.data == null) return Text('no data');
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (_, int index) {
var course_details = snapshot.data.documents[index];
getCourseDetails(course_details['course_id']);
Application Flow
On first build of CourseProgressList the Widget State _CourseProgressListState is loaded. This State in the build method uses a StreamBuilder to retrieve all documents from the firestore. As soon as the documents are received the StreamBuilder attempts to build the ListView using the ListViewBuilder.
In the ListViewBuilder you make the async call to getCourseDetails(course_details['course_id']); which when complete populates two attributes String course_name, subtitle;
The problem starts here
Problem
When you call
setState(() {
course_name = document.data['course_name'];
subtitle = document.data['subtitle'];
});
you trigger a Widget rebuild and so the process starts over again to rebuild the entire widget.
NB. refreshing state of a stateful widget will trigger a widget rebuild
NB. Firestore.instance.collection('Enrolling_Courses').where('student_id', isEqualTo: widget.id).snapshots() returns a stream of realtime changes implying that your List will also refresh each time there is a change to this collection
Recommendations
If you do not need to call the setState try not to call the setState.
You could let getCourseDetails(course_id_pass) return a Future/Stream with the values desired and use another FutureBuilder/StreamBuilder in your ListViewBuilder to return each ListViewItem. Your user may appreciate seeing some items instead of waiting for all the course details to be available
Abstract your request to firestore in a repository/provider or another function/class which will do the entire workload, i.e retrieving course ids then subsequently the course details and returning a Future<List<Course>> / Stream<List<Course>> for your main StreamBuilder in this widget (see reference snippet below as a guide and requires testing)
Reference Snippet
Your abstraction could look something like this but this decision is up to you. There are many software design patterns to consider or you could just start by getting it working.
//let's say we had a class Course
class Course {
String courseId;
String courseName;
String subTitle;
Course({this.courseId,this.courseName,this.subTitle})
}
Stream<List<Course>> getStudentCourses(int studentId){
return Firestore.instance
.collection('Enrolling_Courses')
.where('student_id', isEqualTo: studentId)
.snapshots()
//extract documents from snapshot
.map(snapshot => snapshot?.data ?? [])
//we will then request details for each document
.map(documents =>
/*because this is an asynchronous request for several
items which we are all interested in at the same time, we can wrap
this in
a Future.wait and retrieve the results of all as a list
*/
Future.wait(
documents.map(document =>
//making a request to firestore for each document
Firestore.instance
.collection('Course_Details')
.document(document['course_id'])
.get()
/* making a final transformation turning
each document into a Course item which we can easily pass to our
ListBuilder/Widgets
*/
.then(courseItem => Course(
courseId:document['course_id'],
courseName:
courseItem.data['course_name'],
subTitle:
courseItem.data['subtitle']
)
)
)
)
);
}
References/Resources
FutureBuilder - https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
StreamBuilder - https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
Future.wait - https://api.flutter.dev/flutter/dart-async/Future/wait.html

Flutter Firestore, interacting with user?

Flutter: I'm using Streambuilder to listen to Firestore data which works great.
However, what I would like to do is to notify the user when certain conditions in Firestore changes, let him respond and write that back to Firestore.
Doable (in Flutter/Dart)?
Thanks in advance.
Best,
/j
StreamBuilder(
stream: Firestore.instance.collection('users').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text('Loading...');
List<DocumentSnapshot> documents = snapshot.data.documents;
documents.forEach((doc) {
if (doc['Invited']) {
**// notify current user that he is invited,
// let him reply yes/no, write that value back to Firestore**
}
}
},
);
Is there a point why you dont want to use Firebase Messaging?
1) Firestore triggers a Notification
2) Your App recieve the notification
3) Your app handle the notification, it should be possible that the user dont see the notification in the status bar.
If i understand you correctly you want to update something as soon as the user receives an invitation. You can do this by using code like this:
Future<Null> updateUser{
#required String userId,
bool accepted,
}) {
assert(userId != null && userId.isNotEmpty);
// create map that contains all fields to update
Map<String, dynamic> dataToUpdate = {
"accepted": accepted,
};
// get the reference to the user you want to update
final userRef = references.users(userId);
// update the user
return userRef.updateData(dataToUpdate);
}
You can get the reference from your document by using doc.reference. The Future will complete if the update was successful or terminate with an error if something went wrong.