Firestore Transaction with conditional operations rolling back - flutter

I'm trying to run this following transaction in a flutter application.
var db = Firestore.instance;
var chatReference = db
.collection('Messages')
.document(groupChatId)
.collection("Chats")
.document(docId);
var newChatReference = db
.collection('NewMessages')
.document(groupChatId);
Firestore.instance.runTransaction((transaction) async {
DocumentSnapshot snap;
snap = await transaction.get(newChatReference);
storedTS = snap.data['timestamp'];
//'timestamp' is stored as string in the db
//storedTS will be compared with a given string later on
//update 'content' field (string) with new value
await transaction.update(
chatReference, {
'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
'content': 'My updated content',
},
).then((val) {
print('updated');
}).catchError((e) {
print('updateErr: $e');
});
if (storedTS == myCondition) {
//if true then also update this other document. If not, just ignore it.
//During test if it IS true, both docs are updated as expected.
// If it is NOT true, then the first update completes, since the console
shows
//updated. But in the db the value remains the same, which means it was
//rolled back just after being updated (or so I guess).
await transaction.update(
newChatReference, {
'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
}
).then((val) {
print('newChat TS updated');
}).catchError((e) {
print('updateErr newChat TS: $e');
});
}
}).catchError((e) {
print("rtError: $e");
});
I get a print in the console saying 'updated' but nothing changes in the db in the collection from the first update operation. If I take out the if block in the second update, then it works as expected. Flabbergasted! Hope someone here can point out what I'm doing wrong.

Ok, after lots of digging around I learned that if you READ a doc in a tx, you MUST also do a WRITE on that doc. Beats me why it has to be like that, but for now we'll just have to live with it, I guess.

Related

Why is 'return' not awaiting this nested 'then' in Flutter/Dart?

I'm new to programming, and this is my first post on stack overflow! In building my first Flutter app I'm trying to understand the code, and I'm not sure why two pieces of code behave differently. Feel free to just look at the code and answer why one doesn't work... or if you'd like, here's the background.
Data structure:
Collection: chatters
Document: chatter doc
SubCollection: members-chatter, another SubCollection: approved-chatter
Documents: member doc, and approved doc
I'm listing all the chatter docs that a user is a member of, so from a CollectionGroup query with uid in the member doc, I then lookup the parent doc id. Next I want to have chatter docs be marked bool public, and for !public chatters, I only want them listed if the user's uid is also on an approved doc in SubCol approved-chatter.
So my main question is why the await doesn't hold through the entirety of the nested .then's in my first attempt.
But while I'm here, I'm also open to any insight on my approach to handling membership to groups of both public and approval-required types. It seems trickier than I first thought, once considering read/write permissions and appropriate security and privacy.
I tried this first.
// get my chatter IDs
Future<List<String>> oldgetMyChatterIDs() async {
List<String> myChatterIDs = [];
await FirebaseFirestore.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get()
.then(
(snapshot) => snapshot.docs.forEach((document) {
document.reference.parent.parent!.get().then((value) {
// the above 'then' isn't awaited.
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
// 'myChatterIDs' is returned empty before the above line fills the list.
} else {
// check if user is approved.
}
});
}),
);
// // adding the below forced delay makes the code work... but why aren't the 'thens' above working to holdup the return?
// await Future.delayed(const Duration(seconds: 1));
return myChatterIDs;
}
but return myChatterIDs; completes before:
document.reference.parent.parent!.get().then((value) {
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
}
Why doesn't the return await the await?
I rewrote the code, and this way works, but I'm not sure why it's different. It does appear a bit easier to follow, so I perhaps it's better this way anyway.
// get my chatter IDs
Future<List<String>> getMyChatterIDs() async {
List<String> myChatterIDs = [];
QuerySnapshot<Map<String, dynamic>> joinedChattersSnapshot = await FirebaseFirestore
.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get();
for (var i = 0; i < joinedChattersSnapshot.docs.length; i++) {
DocumentSnapshot<Map<String, dynamic>> aChatDoc =
await joinedChattersSnapshot.docs[i].reference.parent.parent!.get();
bool isPublic = aChatDoc.data()?['public'] ?? true;
if (isPublic) {
myChatterIDs.add(aChatDoc.id);
} else {
try {
DocumentSnapshot<Map<String, dynamic>> anApprovalDoc =
await FirebaseFirestore.instance
.collection('chatters')
.doc(aChatDoc.id)
.collection('approved-chatter')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get();
bool isApproved = anApprovalDoc.data()!['approved'];
if (isApproved) {
myChatterIDs.add(aChatDoc.id);
}
} catch (e) {
// // Could add pending to another list such as
// myPendingChatterIDs.add(aChatDoc.id);
}
}
}
return myChatterIDs;
}
Take a look at this piece of code:
print("1");
print("2");
result:
1
2
The print on both lines is synchronous, they don't wait for anything, so they will execute immediately one after the other, right ?
Take a look at this now:
print("1");
await Future.delayed(Duration(seconds: 2));
print("2");
result:
1
2
This now will print 1, then wait 2 seconds, then print 2 on the debug console, using the await in this case will stop the code on that line until it finishes ( until the 2 seconds pass).
Now take a look at this piece of code:
print("1");
Future.delayed(Duration(seconds: 2)).then((_) {print("2");});
print("3");
result:
1
3
2
this seems to be using the Future, but it will not wait, because we set the code synchronously, so 1 will be printed, it will go to the next line, it will run the Future.delayed but it will not wait for it across the global code, so it will go to the next line and print 3 immediately, when the previous Future.delayed finishes, then it will run the code inside the then block, so it prints the 2 at the end.
in your piece of code
await FirebaseFirestore.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get()
.then(
(snapshot) => snapshot.docs.forEach((document) {
document.reference.parent.parent!.get().then((value) {
// the above 'then' isn't awaited.
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
// 'myChatterIDs' is returned empty before the above line fills the list.
} else {
// check if user is approved.
}
});
}),
);
// // adding the below forced delay makes the code work... but why aren't the 'thens' above working to holdup the return?
// await Future.delayed(const Duration(seconds: 1));
return myChatterIDs;
using then, the return myChatterIDs will execute immediately after the Future before it, which will cause an immediate end of the function.
To fix this with the then, you need simply to move that return myChatterIDs inside the then code block like this:
/*...*/.then(() {
// ...
return myChatterIDs
});
using the await keyword in your second example will pause the code on that line until the Future is done, then it will continue for the others.
you can visualize what's happening live on your code, by setting breakpoints on your method lines from your IDE, then see what's running first, and what's running late.
It´s because document.reference.parent.parent!.get() is a Future too but you aren't telling your function to wait for it. The first await only applies to the first Future.
When you use then you are basically saying, execute this Future and when it's finished, do what is inside this function but everything outside that function doesn't wait for the then to finish unless you told it to with an await.
For example this code:
String example = 'Number';
await Future.delayed(const Duration(seconds: 1)).then(
(value) {
//Here it doesn't have an await so it will execute
//asynchronously and just continues to next line
//while the Future executes in the background.
Future.delayed(const Duration(seconds: 1)).then(
(value) {
example = 'Letter';
},
);
},
);
print(example);
Will result in Number being printed after 1 second. But this code:
String example = 'Number';
await Future.delayed(const Duration(seconds: 1)).then(
(value) async {
//Here it does have an await so it will wait for this
//future to complete too.
await Future.delayed(const Duration(seconds: 1)).then(
(value) {
example = 'Letter';
},
);
},
);
print(example);
Will result in Letter being printed after 2 seconds.
Additional to this, forEach cannot contain awaits as mentioned here in the Dart documentation and even if you use it will still be asynchronous.
Try using
for(var document in snapshot.docs){
//Your code here
}
Instead

Problem while reading document from firestrore in flutter

I am new to Firebase I want to read all data of document here is how i am trying to read
This is my function to get data.
Future<List<AgencyModel>> getAgencyData() async {
List<AgencyModel> agencyListModel = [];
try {
agencyListModel = await _db.collection('colelctionName')
.doc('myDoc')
.snapshots()
.map((doc)=> AgencyModel.fromJson(doc.data()!)).toList();
print('List : ${agencyListModel.length}');
return agencyListModel;
} catch (e) {
debugPrint('Exception : $e');
rethrow;
}
}
This is how i am calling the above function
getAgencyDetails() async {
List<AgencyModel> data = await fireStoreService.getAgencyData();
print('Data : ${data.first}');}
and this is my models class fromjson function
factory AgencyModel.fromJson(Map<String, dynamic> json) {
return AgencyModel(
agencyName: json['agencyName'],
agencyContact: json['agencyContact'],
agencyAddress: json['agencyAddress'],
cnic: json['cnic'],
agencyContactDetails: json['agencyContactDetails'],
certificatesUrl: json['certificatesUrl'],
locationUrl: json['locationUrl'],
earning: json['earning'],
processing: json['processing'],
onHold: json['onHold']);}
I am not getting any error or exception, Also these two print statements does not work not display anything not even the Strings List : and Data : i.e
print('List : ${agencyListModel.length}');
print('Data : ${data.first}');}
UPDATE:
According to the documentation:
https://firebase.google.com/docs/firestore/query-data/get-data#dart_1
It is necessary to distinguish whether you want to retrieve the data only once or listen to changes over the document in real time.
It seems to me like you want to accomplish 1. case that you only want to retrieve data once. In that case.
You should change:
agencyListModel = await _db.collection('collectionName')
.doc('myDoc')
.snapshots()
agencyListModel = await _db.collection('collectionName')
.doc('myDoc')
.get()

Flutter pagination with firestore stream

How to properly implement pagination with firestore stream on flutter (in this case flutter web) ?
my current approach with bloc which is most likely wrong is like this
function called on bloc when load next page, notice that i increased the lastPage variable of the state by 1 each time the function is called:
Stream<JobPostingState> _loadNextPage() async* {
yield state.copyWith(isLoading: true);
try {
service
.getAllDataByClassPage(state.lastPage+1)
.listen((List<Future<DataJob>> listDataJob) async {
List<DataJob?> listData = [];
await Future.forEach(listDataJob, (dynamic element) async {
DataJob data= await element;
listData.add(data);
});
bool isHasMoreData = state.listJobPostBlock.length!=listData.length;
//Update data on state here
});
} on Exception catch (e, s) {
yield StateFailure(error: e.toString());
}}
function called to get the stream data
Stream<List<Future<DataJob>>> getAllDataByClassPage(
String className, int page) {
Stream<QuerySnapshot> stream;
if (className.isNotEmpty)
stream = collection
.orderBy('timestamp', "desc")
.where('class', "==", className).limit(page*20)
.onSnapshot;
else
stream = collection.onSnapshot;
return stream.map((QuerySnapshot query) {
return query.docs.map((e) async {
return DataJob.fromMap(e.data());
}).toList();
});
}
With this approach it works as intended where the data loaded increased when i load next page and still listening to the stream, but i dont know if this is proper approach since it replace the stream could it possibly read the data twice and end up making my read count on firestore much more than without using pagination. Any advice is really appreciated, thanks.
Your approach is not very the best possible indeed, and as you scale you going to be more costly. What I would do in your shoes would be to create a global variable that represents your stream so you can manipulate it. I can't see all of your code so I am going to be as generic as possible so you can apply this to your code.
First let's declare the stream controller as a global variable that can hold the value of your stream:
StreamController<List<DocumentSnapshot>> streamController =
StreamController<List<DocumentSnapshot>>();
After that we need to change your getAllDataByClassPage function to the following:
async getAllDataByClassPage(String className) {
Stream stream = streamController.stream;
//taking out of the code your className logic
...
if(stream.isEmpty){
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.limit(20)
.onSnapshot
streamController.add(snap.docs);
}else{
DocumentSnapshot lastDoc = stream.last;
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.startAfterDocument(lastDoc)
.limit(20)
.onSnapshot;
streamController.add(snap.docs);
}
}
After that all you need to do in order to get the stream is invoke streamController.stream;
NOTE: I did not test this code but this is the general ideal of what you should try to do.
You can keep track of last document and if has more data on the list using startAfterDocument method. something like this
final data = await db
.collection(collection)
.where(field, arrayContains: value)
.limit(limit)
.startAfterDocument(lastDoc)
.get()
.then((snapshots) => {
'lastDoc': snapshots.docs[snapshots.size - 1],
'docs': snapshots.docs.map((e) => e.data()).toList(),
'hasMore': snapshots.docs.length == limit,
});

Flutter- show loading screen till firestore document changes

I am building a ridesharing app with flutter. So far i am stuck on how to communicate between the rider and driver app.
After rider submits pickup request to firestore db, i want the loading screen to show until a driver accepts the request(possibly by updating firestore db) then move to screen with driver info.
if (event is PaymentMadeEvent) {
yield TaxiBookingLoadingState(
state:
PaymentNotInitializedState(booking: null, methodsAvaiable: null));
TaxiBooking booking = await TaxiBookingStorage.addDetails(TaxiBooking.named(paymentMethod: event.paymentMethod));
String docID = await TaxiBookingController.submitRequest(
booking.source.areaDetails,
booking.destination.areaDetails,
[booking.source.position.latitude,booking.source.position.longitude],
[booking.destination.position.latitude,booking.destination.position.longitude],
booking.estimatedPrice,
booking.estimatedDuration,
booking.estimatedDistance
);
booking = await TaxiBookingStorage.addDetails(TaxiBooking.named(dbID: docID));
await TaxiBookingController.generateToken();
TaxiDriver taxiDriver = await TaxiBookingController.getTaxiDriver(booking);
// Timer.periodic(Duration(seconds: 5), (Timer t) async* {
// } );
taxiDriver = await TaxiBookingController.getTaxiDriver(booking);
yield TaxiNotConfirmedState(booking: booking, driver: taxiDriver);
}
static Future<TaxiDriver> getTaxiDriver(TaxiBooking booking) async {
TaxiDriver taxis2;
var driver = await Firestore.instance.collection("rider_pickup_pairing")
// .where(DocumentReference,isEqualTo: booking.dbID)
.where("driver_id",isEqualTo: 'jk')
.getDocuments()
.then((QuerySnapshot snapshot) {
if (snapshot.documents == []) {
taxis2 = null;
} else {
snapshot.documents.forEach((f) =>
taxis2 = TaxiDriver.named(
driverPic:
"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/profilepic.jpg",
driverName: "John Doe",
driverRating: 4.5,
taxiDetails: "Toyota Corolla(ABJ823KU)")
);
TaxiBookingStorage.addDetails(TaxiBooking.named(driver: taxis2.driverName));
}
return taxis2;
});
return driver;
}
You should be using .onSnapshot() instead of .getDocument() in order to achieve this.
The difference between these two methods is that getDocument() will only retrieve the document once while onSnapshot() will keep listening to any event on Firestore. This is covered in these documents: get realtime updates and get data once.
Hope you find this useful.

How do I know if my Firestore transaction failed?

I am updating my document with this code.
Future<void> save() async {
print('league save');
final DocumentReference ref =
Firestore.instance.collection('leagues').document(_documentName);
Firestore.instance.runTransaction((Transaction tx) async {
DocumentSnapshot postSnapshot = await tx.get(ref);
if (postSnapshot.exists) {
await tx.update(ref, _getDocument());
print('league save complete');
}
});
}
I believe that this may be failing sometimes but I am not sure. I am got getting an error.
The reason I suspect it is failing sometimes is because my listener (elsewhere in the app) isn't always getting fired when the document changes.
How do I log or capture an error in the transaction?
runTransaction is just a normal async operation that you can follow up with a then and catchError:
Firestore.instance.runTransaction((Transaction tx) async {
// do whatever
}).then((val) {
// do something upon success
}).catchError((e) {
// do something upon error
});
and you can skip then .then() if you want