How to filter data from backend using bloc future fetch stream? - flutter

I have this method on bloc
fetchProductAttribute(int _prodId) async {
List<ProductAttribute> productAttribute = await _repository.fetchProductAttribute(_prodId).catchError((err) => _fetcher.addError(err));
_fetcher.sink.add(productAttribute);
}
This will call repository then rest api to the backend. Now I want to filter this data.
I will modify this but it's not working... How to do this properly?
fetchProductAttribute(int _prodId) async {
List<ProductAttribute> productAttribute = await _repository.fetchProductAttribute(_prodId).catchError((err) => _fetcher.addError(err));
for(var c in productAttribute){
if(c.variant==true){
_fetcher.sink.add(productAttribute);
}
}
}
All data still coming up...I want only data with variant true to be displayed on screen. How to do this?
I seen one article on how to do filtering on Stream by using StreamSubscription here and here but this is not what I want. I want to filter out from the earlier part of REST.

As one comment said, use the where operator over the list.
fetchProductAttribute(int _prodId) async {
List<ProductAttribute> productAttribute = await _repository.fetchProductAttribute(_prodId).catchError((err) => _fetcher.addError(err));
productAttribute = productAttribute.where((c) => c.variant == true).toList()
_fetcher.sink.add(productAttribute);
}

Related

Riverpod provider list updates from firebase only if the code is in the provider

I have a Riverpod Streamprovider that manages how a number of different Firebase documents are presented to the user. The user can then access each document, make some changes and return to the list of their documents. Once they have made, changes the row for that document should have a tick showing. The only wrinkle is that these documents in a different collection, each with their own identifier. So its not as easy as just streaming a whole collection, my function needs to get the identifier for each item and then get a list of documents to send to the user.
I have the code so it 'just works' but what I can't work out is why updating the record works when all the code is inside the provider vs when the provider calls it a external code. For example this StreamProvider works as I want and updated documents are recognised
final outputStreamProvider = StreamProvider.autoDispose((ref) async* {
final List<itemModelTest> itemList = [];
final user = ref.watch(loggedInUserProvider);
final uid = ref.watch(authStateProvider).value!.uid;
for (String ident in user.value!.idents) {
# get each item by its own identifier
final item = FirebaseFirestore.instance
.collection('items')
.where("ident", isEqualTo: ident)
.limit(1)
.snapshots();
final result = await item.first;
final test = result.docs[0];
final itemItem = itemModelTest.fromFirebaseQuery(test, uid);
itemList.add(itemItem);
# Listen for changes in the items
item.listen((event) async {
dev.log('event changed');
for (var change in event.docChanges) {
if (change.type == DocumentChangeType.modified) {
itemModelTest updatedModel =
itemModelTest.fromFirebaseQuery(test, uid);
itemList
.removeWhere((element) => element.title == updatedModel.title);
itemList.add(updatedModel);
}
}
});
}
yield itemList;
});
But as you can see it contains a lot of logic that doesn't belong there and should be with my firebase database class. So I tried to split it so now in my firebase crud class I have almost identical code:
Stream<List<itemModelTest>> itemsToReviewStream(LoggedInUser user, String uid) async*{
final List<itemModelTest> itemList = [];
for (String ident in user.idents) {
final item = FirebaseFirestore.instance
.collection('items')
.where("ident", isEqualTo: ident)
.limit(1)
.snapshots();
final result = await item.first;
final test = result.docs[0];
final itemItem = itemModelTest.fromFirebaseQuery(test, uid);
itemList.add(itemItem);
item.listen((event) async {
dev.log('event changed ${event.docChanges.first.doc}');
for(var change in event.docChanges){
if(change.type == DocumentChangeType.modified){
itemModelTest updatedModel = itemModelTest.fromFirebaseQuery(test, uid);
itemList.removeWhere((element) => element.title == updatedModel.title);
itemList.add(updatedModel);
}
}
});
}yield itemList;
}
and my StreamProvider now looks like this
// Get a list of the currently logged in users papers to review
final testitemStreamProvider = StreamProvider.autoDispose((ref) {
final user = ref.watch(loggedInUserProvider).value;
final uid = ref.watch(authStateProvider).value!.uid;
return DataBase().itemsToReviewStream(user!, uid);
});
The only problem is using this second approach the updates to firebase don't trigger any updates to the ui so when the user returns to their list of documents they cant see which have been processed already. I have been round the houses trying to work out what I am doing wrong but cant see it.
Edit: just a quick edit in case it matters but this is for FlutterWeb not iOS or Android
I am leaving this in case anyone else has the same problem. The real problem with this project was that the database structure was not fit for purpose and a further restriction was to not duplicate data on the database.
The simplest solution (and if you happen to be reading this because you fixing a similar problem) is to make a copy of the documents the user is supposed to have access to in their own collection, this can then be streamed as an entire collection. Checking which documents have and have not been looked at by users was always going to have to be done via an admin account anyway, so it's not as though this would have incurred a penalty.
All the same to manage my particular data repo i ended up
1 make a simple provider to stream a single document
final getSinglePageProvider = StreamProvider.family((ref, String pageId){
return DataBase().getSinglePage(pageId);});
Then once you have a list of all the documents the user has access to make a provider that provides a list of providers above
final simpleOutputsStreamsProvier = StreamProvider((ref) async* {
final user = ref.watch(loggedInUserProvider);
final items = user.value!.items;
yield items.map((e) => ref.watch(getSinglePageProvider(e))).toList();
});
You can then use this in a consumer as normal, but it has to be 'consumed' twice. In my case, I watched the ListProvider in the build method of a ConsumerWidget. That gives you a list of StreamProviders for individual pages. Finally I used ListView to get each StreamProvide (listofProviders[index]) and unwrapped that with provider.when(...
I have no idea how brittle this approach will turn out to be however!

Flutter Async Function Update Doc and Use In Same Function

I have a function that does several things including update a document where the updated document is then used later on the in the function to update another set of documents. In this case the ChallengesRecord challenge is first updated by adding the current user to the challenge. Then, a second set of documents are updated by updating the list of users that are eventually sent a push notification through a collection called ff_user_push_notifications.
Everything seems to be working except the notifications record is not being updated with the updated challenges doc and pulling in data from the doc before it was updated. Here is what I have
Future<void> joinUserToChallenge(ChallengesRecord challenge) async {
//this updates the ChallengesRecord challenge
final challengesUpdateData = {
'users_inchallenge': FieldValue.arrayUnion([currentUserReference])
};
await challenge.reference.update(challengesUpdateData).then((value) =>
print('success'));
//this then queries the notification documents to be updated then
//iterates through them to update each one on a batch.commit()
//however it is not using the updated challenges doc it appears
final batch = FirebaseFirestore.instance.batch();
await FirebaseFirestore.instance
.collection('ff_user_push_notifications')
.where(filter 1)
.where(filter 2)
.get().then((snapshots) => {
snapshots.docs.forEach((doc) => {
batch.update(doc.reference, {
"user_refs" : challenge
.usersInchallenge.map((u) => u.path).join(',')
}),
print(snapshots.docs.length)
})
});
await batch.commit();
}
Console output below which suggests the challenge doc is being updated first.
flutter: success
flutter: 2
So it appears the batch.update() is not waiting on/using the updated challenge doc. Any suggestions?
UPDATE: I also tried the code below, but it is still not working.
Future<void> joinUserToChallenge(ChallengesRecord challenge) async {
//this updates the ChallengesRecord challenge
final challengesUpdateData = {
'users_inchallenge':
FieldValue.arrayUnion([currentUserReference])
};
await challenge.reference.update(challengesUpdateData).then((value)
=> print('success'));
final batch = FirebaseFirestore.instance.batch();
await FirebaseFirestore.instance
.collection('ff_user_push_notifications')
.where(filter 1)
.where(filter 2)
.get();
pushNotificiations.docs.forEach((doc) => {
batch.update(doc.reference, {
"user_refs" : challenge
.usersInchallenge.map((u) => u.path).join(',')
}),
});
await batch.commit()
}

Flutter / RiverPod - how to load StateProvider initial state from Firestore

I'm stuck with a situation where I am trying to use a RiverPod provider in my Flutter app to represent user preference data. In this case, the user preference data is stored in FireStore.
I'm stuck with understanding how to load provider state from Firestore. Currently, I'm trying to use the "userPreferencesFutureProvider" to load the 'GdUserPreferences" data from a service that calls Firestore, which then pushes the data into "userPreferencesProvider" using the 'overrideWith' method. However, when I access the user preference data via the 'userPreferencesProvider' provider the data loaded from Firestore is not present
final userPreferencesFutureProvider = FutureProvider<bool>( (ref) async {
final p = ref.watch(userPrefsServiceProvider);
GdUserPreferences? aPrefs = await p.load();
if (aPrefs == null) {
aPrefs = GdUserPreferencesUtil.createDefault();
}
userPreferencesProvider.overrideWith((ref) => UserPreferencesNotifier(p,aPrefs!));
return true;
});
final userPreferencesProvider = StateNotifierProvider<UserPreferencesNotifier,GdUserPreferences>((ref) {
return UserPreferencesNotifier(ref.watch(userPrefsServiceProvider),GdUserPreferences());
});
Any suggestions?
Update
Further to the feedback received I have updated my code as shown below, but this still doesn't work...
final userPreferencesFutureProvider = FutureProvider<bool>( (ref) async {
// get service that wraps calls to Firestore
final p = ref.watch(userPrefsServiceProvider);
// load data from Firestore
GdUserPreferences? aPrefs = await p.load();
// if none found then create default values
if (aPrefs == null) {
aPrefs = GdUserPreferencesUtil.createDefault();
}
// push state into a provider that will be used
ref.read(userPreferencesProvider.notifier).update(aPrefs);
// this future returns a boolean as a way of indicating that the data load succeeded.
// elsewhere in the app access to the user preference data is via the userPreferencesProvider
return true;
});
final userPreferencesProvider = StateNotifierProvider<UserPreferencesNotifier,GdUserPreferences>((ref) {
print('default provider');
return UserPreferencesNotifier(ref.watch(userPrefsServiceProvider),GdUserPreferences());
});
class UserPreferencesNotifier extends StateNotifier<GdUserPreferences> {
// service is a wrapper around FireStore collection call
final GdUserPreferencesService service;
UserPreferencesNotifier(this.service, super.state);
void update(GdUserPreferences aNewPrefs) {
print('update state');
state = aNewPrefs;
}
}
The purpose of having a separate FutureProvider and StateNotifierProvider, is that I can insert the FutureProvider when the app first loads. Then when I want to access the user preference data I can use the straight forward StateNotifierProvider, which avoids the complications of having Future Providers all over the app.
Note: using the print methods I can show that the 'update' method is called after the userPreferencesProvider is first created, so I can't understand why the data is not updated correctly
Apologies to all responders...
The problem was caused by a coding error on my side. I had two versions of 'userPreferencesProvider' defined in two different locations. Taking out the rogue duplicate definition and it now works fine.

Cannot get subcollection along with collection in flutter

I am trying to get documents with their own subcollections, from Stream, but I am stuck.
This is where I set up my StreamSubscription:
Future<void> _toggleOrdersHistorySubscription({FirebaseUser user}) async {
_ordersHistorySubscription?.cancel();
if (user != null) {
_ordersHistorySubscription = ordersRepository
.ordersHistoryStream(userId: user.uid)
.listen((ordersSnapshot) {
final List<OrderModel> tmpList = ordersSnapshot.documents.map((e) {
// e.reference.collection("cart").getDocuments().;
return OrderModel.orderFromSnapshot(e);
}).toList();
add(OrdersHistoryUpdated(ordersHistory: tmpList));
});
}
}
The issue is that I can't see a way to get subcollection along with the parent document because getDocuments returns a Future.
Anyone can clear this issue for me?
So, I update the code method a separate method for retrieving data when listener is triggered but it doesn't work fully and I do not understand what's happening and why part of the code is working and part is not.
List<OrderModel> _getOrdersHistory({
#required QuerySnapshot snapshot,
}) {
return snapshot.documents.map((document) {
List<OrderedProductModel> cart = [];
List<AddressModel> addresses = [];
document.reference.collection("cart").getDocuments().then((snapshot) {
snapshot?.documents?.forEach((doc) {
cart.add(OrderedProductModel.fromSnapshot(doc));
});
});
document.reference
.collection("addresses")
.getDocuments()
.then((snapshot) {
snapshot?.documents?.forEach((doc) {
addresses.add(AddressModel.addressFromJson(doc.data));
});
});
final order = OrderModel.orderFromSnapshot(
document,
restaurantCart: cart,
);
return order.copyWith(
orderAddress:
(addresses?.isNotEmpty ?? false) ? addresses.first : null,
sentFromAddress:
(addresses?.isNotEmpty ?? false) ? addresses.last : null,
);
})
.toList() ??
[];
}
As an alternate solution to my original issue is that I made a map entry in Firestore instead of a collection for 2 address documents (which are set above as orderAddress and sentFromAddress) and for the cart I decided to get the data when needed for every cart item.
So the method which I put in the update is not the final one, but I want to understand what is happening up there as I do not understand why:
Why the cart is shown as empty if I do a print(order); right before the return and in the bloc it has data;
Why the orderAddress and sentFromAddress are both empty no matter what I try;
To be short: You'll never be able to get a List synchronously if you get the data async from firebase.
Both questions have the same answer:
Your timeline:
For each document
Create an empty list
Initiate the firebase query - getDocuments()
Subscribe to the returned future with - .then((snapshot){cart.add(...)}).
This lambda will be invoked when the documents arrived.
Another subscribe
Save your empty cart and first/last of empty addresses to an OrderModel
Your List contains the references to your empty lists indirectly
Use your bloc, some time elapsed
Firebase done, your callbacks starts to fill up your lists.
Regarding your comment like stream.listen doesn't like async callbacks:
That's not true, you just have to know how async functions work. They're run synchronously until the first await, then return with an incomplete future. If you do real async things you have to deal with the consequences of the time delay like changed environment or parallel running listeners.
You can deal with parallel running with await for (T v in stream) or you can use subscription.pause() and resume.
If anything returns a future, just do this:
...getDocuments().then((value) => {
value is the item return here. Do something with it....
})
Also, you might want to split your method up a bit to share the responsibility.
If getDocuments is a Future function and you need to wait for it, I think you should add await before it. I also don't see the snapshot status checking in your code pasted here. Maybe you have already checked the snapshot status in other function? Make sure the snapshot is ready when using it.

How to test async functions that update PublishSubject or BehaviorSubject object (RxDart) in Flutter

I've been learning flutter for a few weeks and come from an Android background so far I love it and I have also been delighted to find that Flutter was designed with testing in mind from day one. However, I've been having an issue running the following test.
main() => {
test('test get popular repos', () async {
final testOwner = Owner(1010, "testLink");
final testRepo =
Repo(101, testOwner, "testRepo", "description", 'htmlUrl', 500);
final testRepoResponse = RepoResponse(List.from([testRepo]), null);
final uiModel = PopRepo(testRepo.owner.avatarUrl, testRepo.name,
testRepo.description, "Stars: ${testRepo.stargazersCount}");
final searchData = SearchData(List.from([uiModel]), null);
final Repository mockRepository = _mockRepository();
when(mockRepository.getPopularReposForOrg("org"))
.thenAnswer((_) => Future.value(testRepoResponse));
final repoSearchBloc = RepoSearchPageBloc(mockRepository);
await repoSearchBloc.getPopularRepos("org");
await expectLater(repoSearchBloc.resultSubject.stream, emits(searchData));
}),
};
class _mockRepository extends Mock implements Repository {}
My RepoSearchBloc takes data from a Repository and transforms it into the Ui model. Finally it posts that now UI-ready data to the Subject
this is the method under test in the RepoSearchBloc
getPopularRepos(String org) async {
if (org == null || org.isEmpty)
return resultSubject.add(SearchData(List(), null));
RepoResponse response = await _repository.getPopularReposForOrg(org);
if (response.error == null) {
List<Repo> repoList = response.results;
repoList.sort((a, b) => a.stargazersCount.compareTo(b.stargazersCount));
var uiRepoList = repoList
.map((repo) => PopRepo(repo.owner.avatarUrl, repo.name,
repo.description, "Stars: ${repo.stargazersCount}"))
.take(3)
.toList();
resultSubject.add(SearchData(uiRepoList, null));
} else {
ErrorState error = ErrorState(response.error);
resultSubject.add(SearchData(List(), error));
}
When I run the test I keep getting this message no matter what I do it seems with either BehaviorSubject or PublishSubject:
ERROR: Expected: should emit an event that <Instance of 'SearchData'>
Actual: <Instance of 'BehaviorSubject<SearchData>'>
Which: emitted * Instance of 'SearchData'
Any ideas how to get this test to pass?
Ended up figuring this out with the help of a user Nico #Rodsevich of the Flutter Glitter community
anyways using his suggestion to use await for
I came up with the following solution which passed
await for (var emittedResult in repoSearchBloc.resultSubject.stream) {
expect(emittedResult.results[0].repoName, testRepo.name);
return;
}
The RxDart library has some subject tests for reference but my subject being posted to asynchronously did not adhere to their test cases so this solution ended up being just what I needed.
Also #Abion47 's comment also seems to do the job when I move async inside the parameter for expected
expectLater( (await repoSearchBloc.resultSubject.stream.first as SearchData).results[0].repoName, testRepo.name);