How do I know if a transaction on Firestore succeded? - flutter

Is there any method to know if the transaction was successful? I need to implement a loading "widget animation" if I upload large archives and spend too much time on that. And then change the screen after success, but I don't know-how.
Thanks!
Transaction Example:
CollectionReference reference = Firestore.instance.collection("collection_example");
Firestore.instance.runTransaction((Transaction transaction) async {
await transaction.set(reference, {
"index_1":"ABC",
"index_2": 2,
"index_3": {"mapIndex_1": "ABC"}
});
});

You cannot receive that from your runTransaction call in this case because it returns a Future<Map<String, dynamic>>. As I have checked, it will always return an empty Map. Thus there is nothing to get from the runTransaction function itself.
You can easily get a Stream of updates from your reference though, which would look something like this:
DocumentReference documentReference;
firestore.runTransaction( // firestore in this case is your Firestore instance
(Transaction transaction) async {
// whatever you do here with your reference
await transaction.update(
documentReference,
...
);
documentReference.snapshots().listen((DocumentSnapshot event) {
// here you could e.g. check if the transaction on your reference was succesful
});
As you can see I used the snapshots() Stream<DocumentSnapshot) on the same DocumentReference as the transaction was run on. The Stream will update as soon as the transaction has completed and the server reached back to you.
To see why transaction results cannot be evaluated client-side check the answers on my question here.

Related

Create an account and a document in Firestore at the same time?

I'm using Flutter and Firebase for my app and the following is the code for my register function:
Future registerWithEmailAndPassword(String email, String name, String password) async {
try{
// Creates user account with Firebase Auth:
UserCredential result = await _auth.createUserWithEmailAndPassword(email: email, password: password);
User user = result.user!;
// Creates a new document in Firestore with the uid:
await DatabaseService(uid: user.uid).createUserData(
name: name,
email: email,
);
return _userObjectFromUser(user);
} on FirebaseAuthException catch(e) {
return e;
}
}
It works well. However, I keep wondering if this is the best way to do this... What if the connection gets interrupted after creating the account but before creating the documents in Firestore? What if the creation of the document fails for some reason? Then the user would be in a weird situation where they have an account but no data saved in the database, meaning the app would probably load forever.
So, I wonder: is there a way to create something similar to a batch write that would somehow create an account at the same time as the documents are created?
I guess you shouldn't be concerned about this since the two methods will run on each other, they're in a really small chance of this happening, either both will succeed or both will fail together, however, I can recommend for those cases to listen to the authStateChanges() stream and take an action based on it, combined with using the isNew like this :
// At first, we were not sure that the document exists
bool areWeSureThatTheuserHaveDocument = false;
// we listen to auth changes of new user
FirebaseAuth.instance.authStateChanges().listen((user) {
// we want this method to get triggered only when the user authenticates, we don't want it to get executed when the user signs out
if(user != null && !areWeSureThatTheuserHaveDocument) {
// here we create the document
await DatabaseService(uid: user.uid).createUserData(
name: name,
email: email,
);
// now if the document does exists, it will return true, for future checks on this method it will not be executed
areWeSureThatTheuserHaveDocument = await doesUserDocumentExists(user.uid);
}
});
// this is the check document existence
Future<bool> doesUserDocumentExists(String id) async {
final collection = await FirebaseFirestore.instance.collection("users").get();
return collection.docs.map((doc) => doc.id).contains(id);
}
Actually, if you're willing to implement this code or something similar to it, you might want to know that by this you can make sure 100% of the user has a document in the database, but it will cost you one additional read to check that existence od document.
Since you tagged with google-cloud-functions, doing the create-user-and-write-profile-document would reduce the chances of having the type of interruption that you talk about.
But my approach is typically to either write the profile document each time the onAuthState changed listener for a user gets a value, or to check for the existence of a document at that time and create it if needed.

Flutter/Dart: return not waiting "toList" with async function to finish

I'm working on an API sync using Flutter and SQFLite.
I get the data from the API, map it, and then add to SQL item by item:
return (response as List).map((tmp) {
print('• API: apiSync - Inserting News');
DBProvider.db.updateNews(ArtNews.fromJson(tmp));
}).toList();
Everything works fine, but there is a small problem: the return is called before the 'updateNews' is done.
updateNews is a simple async function that writes data on SQFLite.
This creates a big issue: the app opens before the sync is over and data is still being written on DB.
I've tried using AWAIT, but it doesn't make any effect:
(response as List).map((tmp) async{
print('• API: apiSync - Inserting $type');
await DBProvider.db.updateNews(ArtNews.fromJson(tmp));
}).toList();
return 'done';
updateNews is called dozens of times, the toList ends, returns 'done', but the updateNews is still adding content to the DB.
I guess there is something I'm missing on toList or async. Any ideas? Thanks!
Try this:
await Future.wait((response as List).map((tmp) {
print('• API: apiSync - Inserting $type');
return DBProvider.db.updateNews(ArtNews.fromJson(tmp));
}));
return 'done';
Gist for reference:
https://dartpad.dev/?id=e72b1dc9013e2add447a90979bed7aab

Reset flutter cache from firebase

I have a code that takes data from firebase. The problem is that i am caching the data once i am getting it and than checking if i cached it the next time i want to take it so i can reduce loading time.
The problem is that when i am changing something in the database, when i refresh the app i still see the old data because i think that it is getting it from the cache.
How can i make it so that when i reset the app, i clear the cache and get the new data?
here is a piece of code that checks if the data is cached or not.
List lists = [];
print(id);
await firestoreInstance
.collection("menu/" + id + "/" + id)
.get(GetOptions(source: Source.cache))
.then((querySnapshot) {
querySnapshot.docs.forEach((result) async {
lists.add(result.data()['name']
);
});
});
if (lists.isEmpty) {
print('aici');
await firestoreInstance
.collection("menu/" + id + "/" + id)
.get()
.then((querySnapshot) {
querySnapshot.docs.forEach((result) async {
lists.add(result.data()['name']);
});
});
return lists;
} else {
return lists;
}
}
To clear the cache, you can call the clearPersistence method, which clears both cached reads and pending writes.
I'd usually recommend against trying to do your own cache management though, and instead let Firestore handle it. This also means I wouldn't use GetOptions and instead just use a regular get, or (even better) a realtime snapshot listener which lets Firestore manage the cache best.
you are only checking if the list is empty, you have to check if the newly added data is available in the list, if not available, it will fetch the data from firebase again and if it is available than it will show data from previously stored data list.

FireStore read fails silently and I have no idea why

Help is much appreciated how to trace down this issue, because I am running out of ideas.
I am calling the function getOrderCollection, below, but it aborts after the first line var myCompanyDoc = await FirebaseFirestore.instance.collection('companies').doc(myCompany).get(); Without trowing anything to the console or jumping into some library when debugging. When I click next statement it jumps back to the calling function.
I am authenticated to the database, companyCollection = FirebaseFirestore.instance.collection('companies') provides an initialized object pointing to the collection and myCompany is a constant with the document id entered by copy/paste.
If some rules for the database but I can't see successful or denied queries with the monitor.
Any ideas how I can proceed tracing down the issue?
Future<void> getOrderCollection() async {
var myCompanyDoc = await FirebaseFirestore.instance.collection('companies').doc(myCompany).get();
print("companyDoc fetched");
final myDeliveryDocRef = myCompanyDoc.data()['delivery'].toString();
orderCollection = FirebaseFirestore.instance.collection('companies').doc(myCompany).collection('features').doc(myDeliveryDocRef).collection('orders');
orderBriefDoc = FirebaseFirestore.instance.collection('companies').doc(myCompany).collection('features').doc(myDeliveryDocRef);
}
UPDATE: This is collection > document what corresponds to final String myCompany = '4U4kZKXkr3rHA6B04S5K';
As we discussed in your comments, the issue was that you forgot to await the getOrderCollection() function. Even though, as you mentioned, your caller function _deliveryRepository.initRepository() was awaited, you still had to await getOrderCollection() inside your caller method to make sure that the code is waiting for the getOrderCollection() to be executed before it proceeds to the next line.
In general, you want to have some error handling and to type the known types/classes (avoid using var).
Error handling - for async/await place the code inside a try/catch.
Typing - Dart is type safe, which is really great to prevent runtime errors.
Depending on your setup, you might be able to hover over the Firestore.instance.collection(...).doc(...) to see the return type. .doc(...).get() returns a DocumentSnapshot and .collection(...).get() returns a CollectionSnapshot.
Using the above, it should be easier to debug:
Future<void> getOrderCollection() async {
try {
DocumentSnapshot myCompanyDoc = await FirebaseFirestore.instance.collection('companies').doc(myCompany).get();
print("companyDoc fetched");
final myDeliveryDocRef = myCompanyDoc.data()['delivery'].toString();
} catch(e) {
print('Error: ' + e.toString());
}
}
Don't forget to await your other 2 Firestore queries.

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.