Flutter Async Function Update Doc and Use In Same Function - flutter

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

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-Firestore: Update Multiple Docs At Once

I am pretty new to Flutter and I have spent some time on this and cannot find a clear answer for what I am trying to do. I am querying a collection for a couple of docs and then trying to update them using the batch function. However, I am having trouble understanding the documentation and previous answers on how exactly to use the batch function in Flutter with Firestore with multiple docs. The query seems to be working and returning the docs, but the batch.update() is throwing an error that I cannot figure out how to solve. Here is what I have
InkWell(
onTap: () async {
final batch = FirebaseFirestore.instance.batch();
var pushNotifications = await FirebaseFirestore.instance
.collection('collection name')
.where(first filter)
.where(second filter)
.get();
print(pushNotifications.docs.length);
//this is where the error is occuring when trying to update the batch
for (var postDocs in pushNotificiations.docs) {
batch.update(postDocs.reference, {
"user_refs" : usersInchallenge
}
);
await batch.commit();
)
UPDATE: I am getting closer but now it is only updating the first document of the batch and not the rest. It is giving the error below with the udpated code snippet.
[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: Bad state: This batch has already been committed and can no longer be changed.
After banging my head against a wall for a few hours, here is what finally worked with batch() after querying multiple docs.
final batch = FirebaseFirestore.instance.batch();
var pushNotificiations = await FirebaseFirestore.instance
.collection('ff_user_push_notifications')
.where(filter 1)
.where(filter 2)
.get();
print(pushNotificiations.docs.length);
pushNotificiations.docs.forEach((doc) => {
batch.update(doc.reference, {
"user_refs" : usersInchallenge
}),
});
batch.commit();

Flutter, getting database records and then internet json

I have a simple table from which I'm fetching a list of records. Once I get the records, then I have to get information online for each of the records. The code to do this is as follows:
class UserStationList {
List<UserStationListItem> _userStations = [];
final StreamController<HomeViewState> stateController;
UserStationList({#required this.stateController});
Future fetchUserStations() async {
stateController.add(HomeViewState.Busy);
//Fetch stations from table.
List<Map<String, dynamic>> stations =
await UserStationDatabase.instance.queryAllRows();
//If there are no stations, return and tell the screen to display the no data message.
if (stations.length == 0) {
stateController.add(HomeViewState.NoData);
return;
}
//Loop through each of the stations in the list and build the collection.
stations.forEach((station) async {
UserStationListItem newItem =
await _getPurpleAirSiteData(station['_id'], station['stationid']);
_userStations.add(newItem);
});
//When done, let the screen know.
stateController.add(HomeViewState.DataRetrieved);
}
Future<UserStationListItem> _getPurpleAirSiteData(
int id, int stationId) async {
var response = await http.get('$kURL$stationId');
var data = json.decode(response.body);
return UserStationListItem(
id: id, stationId: stationId, stationName: data['results'][0]['Label']);
}
}
The problem that I am running into involves the futures. I am processing the loop in a forEach and calling into the _getPurpleAirSiteData function for each. Within that function I have to await on the http.get to bring in the data. The stateController.add(HomeViewState.DataRetrieved) function is being called and the function exits long before the loop is completed. This is resulting in the data not being available when the StreamBuilder that I have receiving the data is run.
How can I set this up so that the loop runs completely before calling stateController.add?
I would change this part of code to a list of Futures and await-ing on it.
//Loop through each of the stations in the list and build the collection.
stations.forEach((station) async {
UserStationListItem newItem =
await _getPurpleAirSiteData(station['_id'], station['stationid']);
_userStations.add(newItem);
});
To:
List<Future<UserStationListItem>> listOfFutures = [];
stations.forEach((station) {
listOfFutures.add(_getPurpleAirSiteData(station['_id'], station['stationid']));
});
var stationItems = await Future.wait(listOfFutures);
stationItems.forEach((userStationListItem) {
_userStations.add(userStationListItem);
});
What I am essentially doing creating a list of Futures with your server request. Then await on it which returns a list of item result Maintaining index, which in turn ensures that requests are completed before you hit statecontroller.add. You also gain a performance gain since all request are not going one by one and instead asynchronously. Then you just iterate through the future result and add it to your item list.

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);

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

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