Flutter getx: How to send data between pages from FirestoreQueryBuilder - flutter

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;

Related

How can I get a transformed Stream<List<Object>> to display data with asynchronous code?

I am having trouble getting a stream with a list of objects to populate in a ViewModel.
Load an asynchronous Stream<List<Habit>> from a Firestore Service
in a DailyViewModel.
Call a transform method to turn that stream into a Stream<List<HabitCompletionViewModel>> that looks at a specific
instance of each of my habits for today, and creates that instance if one doesn't exist. There are a few component pieces to this:
For each habit in the initial stream, run a private method that checks if there is an instance of the habit for today, and initializes one if not. This is an asynchronous method because it calls back to the database to update the habit with the new instance.
Then find the instance for today, and return a HabitCompletionViewModel with the habit and today's instance.
Map these to a list.
That new stream is set in a property in the DailyViewModel as todaysHabits.
todaysHabits is called as the stream in a StreamBuilder in the DailyView widget.
The issue I am running into is that I know a completion for today is being found.
I am very fuzzy on what can/should be called as asynchronous code, and whether I'm using correct async/async* return/yield statements in my code, especially since I am trying to kick this process off as part of my constructor function for the DailyViewModel. I've used a bunch of print comments and it seems like everything is running, but the todaysHabits in my ViewModel is always set to null, and the StreamBuilder doesn't return any items.
Is there anything off in my code that could be causing this?
The DailyViewModel has this todaysHabits property, which is loaded near the bottom of the constructor function:
late Stream<List<HabitCompletionViewModel>> todaysHabits;
DailyViewModel({required WeekDates week}) {
_log.v('initializing the daily viewmodel');
_week = week;
_habitService
.loadActiveHabitsByUser(_loginAndUserService.loggedInUser!.id!)
.then(
(activeUserHabits) {
todaysHabits = _getTodaysHabits(activeUserHabits);
_log.v('todaysHabits has a length of ${todaysHabits.length}');
},
);
setBusy(false);
}
That constructor calls this _getTodaysHabits function which is supposed to convert that Stream<List<Habit>> into a Stream<List<HabitCompletionViewModel>>:
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap((habitsList) {
return Stream.fromIterable(habitsList).asyncMap(
(habit) async {
await _updateHabitWithNewCompletions(habit);
HabitCompletion completion = habit.completions!.firstWhere(
(completion) => completion.date
.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
},
).toList();
});
}
And my view (which is used the Stacked package to display the contents of the ViewModel and update accordingly) contains this StreamBuilder that should be returning a list of tiles for each HabitCompletionViewModel:
StreamBuilder<List<HabitCompletionViewModel>>(
stream: vm.todaysHabits,
builder: ((context, snapshot) {
if (snapshot.hasData == false) {
return Center(child: Text('No Habits Found'));
} else {
return Column(children: [
ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => HabitCompletionTile(
key: ValueKey(snapshot.data![i].habit.id),
vm: snapshot.data![i],
),
),
]);
}
})),
Based on pskink's comment, I made the following updates that seem to work. (There is a slight lag when I switch to that view), but it is now showing the correct data.
Basically seems like the issue was that my previous code was returning a list of futures, instead of just a list of HabitCompletionViewModels, and using Future.wait waits for all those to complete.
Pulled out the mapping from List to List into a separate sub-method (here is the main method):
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap(
(habitsList) async => await _mapHabitsToViewModel(habitsList));
}
Updated that sub-method so it first returns a List<Future>, and then uses Future.wait to wait for those to complete as HabitCompletionViewModels before returning that new list:
Future<List<HabitCompletionViewModel>> _mapHabitsToViewModel(
List<Habit> habitsList) async {
List<Future<HabitCompletionViewModel>> newList =
habitsList.map((habit) async {
HabitCompletion completion = habit.completions!.firstWhere((completion) =>
completion.date.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
}).toList();
List<HabitCompletionViewModel> transformed = await Future.wait(newList);
return transformed;
}

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()));
},

How to create recommended ListView in flutter and fire store

There's a way to create a recommended from user ListView using fire base and flutter...
For example I have a list of data in firebase that I am fetching them from firebase as I show them in the data list screen, and I have a list of recommended list view from user for example the clicked data item from user something shows like the below image:
To be more specific how figure if the data was viewed by user or not?
There's a way or docs to do something like this?
In case your intention is to provide some kind of "user likes" functionality.
You can create Provider of ChangeNotifier with Provider package at root (for example) and store Set<YourShopItem> there.
Then expose methods like add(YourShopItem item) and remove(YourShopItem item) on this ChangeNotifier which should add and remove items from your set and call notifyListeners() every time you call this method.
Then when you need to determine if your item is liked just obtain your ChangeNotifier and check if item is in set. Your widgets is gonna be updated every time add or remove methods are called because of their subscription to ChangeNotifier through Provider.
In case your intention is to track visibility of item.
You can use visibility detector package to track whether certain widget is visible. You can subscribe to certain widget and when it's shown, a callback is gonna be fired. So you should wrap every widget of your list into VisibilityDetector widget and save that your item was viewed.
Something like that should do the job:
final List<String> entries = <String>['A', 'B', 'C'];
final List<int> colorCodes = <int>[600, 500, 100];
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: entries.length,
itemBuilder: (BuildContext context, int index) {
return VisibilityDetector(
onVisibilityChanged: (VisibilityInfo info) {
if (info.visibleFraction == 1) {
ON_YOUR_ITEM_IS_VISIBLE_FUNCTION();
}
},
child: Container(
height: 50,
color: Colors.amber[colorCodes[index]],
child: Center(child: Text('Entry ${entries[index]}')),
),
);
}
);
Also refer to this: https://stackoverflow.com/a/63577928/13737975.

Getting Bool State from Firestore in Dart

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.

How to initialize and load data when moving to a screen in flutter?

I want to populate my lists by making API calls when moving to a screen in flutter. I tried calling the async function in the init state however it takes time to load the data of course, and that leads to a null error.
I tried using a future builder in my view. However, I want to make multiple API calls and I don't think that is possible with one future builder. And I would like to avoid having different future builders for each list.
Is there a neat way to do this?
Also is it advisable to load data and pass it on from the previous screen.. since I would like to avoid the loading time?
current code
FutureBuilder(
future: getAllData,
initialData: [],
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data.isEmpty) return Center(child: CircularProgressIndicator(valueColor: new AlwaysStoppedAnimation<Color>(Colors.red),));
else {
return createNewTaskView(context, snapshot.data);
}
}),
init method
#override
void initState() {
this.getAllData = getSocieties();
}