Why Flutter "await" doesn't wait? - flutter

I'm working on a Flutter app which needs uploading image files to Firebase Storage and then saving urls to Firestore.
Below is my code:
void onSubmit() async {
final fireStore = FirebaseFirestore.instance;
//picList has one element.
List<String> picUrlList = [];
picList.forEach((element) async {
//picUrlList.add(await uploadImage(File(element)));
var url = await uploadImage(File(element));
picUrlList.add(url);
print('1');
});
await fireStore.collection('xxx').doc('yyy').update({
"picUrlList": FieldValue.delete(),
});
await fireStore.collection('xxx').doc('yyy').update({
"picUrlList": FieldValue.arrayUnion(picUrlList),
});
print('2');
print(picUrlList.length);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => EditPost()),
(Route<dynamic> route) => false,
);
}
Future<String> uploadImage(var imageFile) async {
//var uuid = Uuid().v1();
Reference ref = FirebaseStorage.instance.ref(
user.uid + DateTime.now().millisecondsSinceEpoch.toString() + '.jpg');
await ref.putFile(imageFile);
return await ref.getDownloadURL();
}
I was expecting print('1') to be executed before print('2'). But the printout order is 2, 0, 1.
0 is the length of the list, which I hope can be full of urls just added.
I just don't know what's wrong with the code. Somebody help me.
Thanks.

Your code will be executed normally till the end and then the callbacks from forEach will start executing, so solve this you have two solutions:
Replace forEach by for loop
Use await Future.forEach(picList, (element) async { var url = await uploadImage(File(element));... });

Related

Why is (list.length == otherList.length) showing false - Flutter

I have a List<File> imageFiles made up of files that a user selects and when they press submit, each of the files is uploaded to the collection in Firebase Storage. For each one, the downloadUrl is added to another list uploadedImages. I cannot upload the document to Firestore until all the downloadUrls are added to the uploadedImages list so I used an if statement if (imageFiles.length == uploadedImages.length) {<upload document>}. The problem is, in those lists are not the same length until the forEach block completes, and even then it doesn't return true. I confirmed that all the files are uploaded to Firebase Storage and printed the uploadedImages so I know they are eventually the same length but don't know how to make the if statement recognize that. Here is my current code:
List<String> uploadedUrls = [];
/// Uploads each file and adds its URL
imageFiles.forEach((file) async {
String fileName = '${UniqueKey()}_post.png';
TaskSnapshot uploadTask = await FirebaseStorage.instance
.ref()
.child('/${user.uid}/posts/$postId/$fileName')
.putFile(file);
String url = await uploadTask.ref.getDownloadURL();
setState(() {
uploadedUrls.add(url);
});
print(uploadedUrls);
});
/// Uploads post if all files and URLs are added *NOT WORKING*
if (imageFiles.length == uploadedUrls.length) {
Post post =
Post(postId, uploadedUrls, user.uid, profile, location, Timestamp.now());
try {
FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.collection('posts')
.doc(postId)
.set(post.toJson()).then((value) {
setState(() {
_isLoading = false;
});
});
} on FirebaseAuthException {
setState(() {
_isLoading = false;
});
}
}
Because you are running an asynchronous function in forEach,it won’t wait until the task is completed. So you can either move the if condition inside the forEach’s callback below the setState or use a parent function as wrapper for both them, and it should await the first call
imageFiles.forEach((file) async {
String fileName = '${UniqueKey()}_post.png';
TaskSnapshot uploadTask = await FirebaseStorage.instance
.ref()
.child('/${user.uid}/posts/$postId/$fileName')
.putFile(file);
String url = await uploadTask.ref.getDownloadURL();
setState(() {
uploadedUrls.add(url);
});
if (imageFiles.length == uploadedUrls.length) {
....
});
or extract the forEach loop into a async function
void addDownloafUrls() asyn {
imageFiles.forEach((file) async { ... });
}
// And you can await this function
addDownloafUrls().then(() {
if (imageFiles.length == uploadedUrls.length) {...}
})

Flutter Future Async Function - await stuck

I am calling a Future function that should add a document to firestore first, then to retrieve the doc id, then update the doc again with some paramaters.
I need to show a dialog for success or faliure.
it is working fine for success case, but incorrect when testing failure .. I disconnect the internet before pressing the button then I press it to call the future function while internet is off to test failure scenario.
the problem is the future function stuck on the trying to add the document to firestore, it never fail and go to the failure scenario.
the below is future function code:
Future CreateAccount() async {
CollectionReference businessprofilescollectionref =
FirebaseFirestore.instance.collection('BusinessProfilesCollection');
DocumentReference docRef = await businessprofilescollectionref.add({
'Profile_city': bpaddSelectedcity,
'Profile_direct_category': directcategory,
});
if (docRef.id.isEmpty) {
showDialog(context: context, builder: (_) => FailureDialog(context));
}
if (docRef.id.isNotEmpty) {
String documentid = docRef.id.toString();
print(documentid);
FirebaseStorage storage = FirebaseStorage.instance;
Reference ref = storage
.ref()
.child("BusinessProfileImages/$documentid".toString());
UploadTask uploadTask = ref.putFile(bpaddimagefile);
final snapshot = await uploadTask.whenComplete(() {});
print("Image Uploaded");
final urldownload = await snapshot.ref.getDownloadURL();
print(urldownload);
businessprofilescollectionref
.doc(documentid)
.update({'Profile_image_link': urldownload});
print("Link Updated");
await showDialog(
context: context, builder: (_) => SuccessDialog(context));
Navigator.of(context, rootNavigator: true).pop();
}
}
I tried to use timeout method like the below, this made the await ends, however the whole Future function is aborted and it doesn't continue executing the commands of showing the failure dialoge.
DocumentReference docRef = await businessprofilescollectionref.add({
'Profile_city': bpaddSelectedcity,
'Profile_direct_category': directcategory,
}).timeout(Duration(seconds: 20));
how can I solve this? I need to try to add the data to database, if that fails, I need to go to failure scenario and show a dialog.
What went wrong:
You're checking if the ID of the DocumentReference is empty and using that as your failure scenario.
That will not work because when you get a DocumentReference back, it will have an ID. Here is a quote from the docs:
Returns a DocumentReference with an auto-generated ID, after
populating it with provided data.
The above reason is why the docRef.id.isEmpty if statement is never executed because the ID is never empty.
How to fix it:
You should use a try-catch to wrap the add operation and implement the error scenario in the catch block.
Then you continue the rest of the operation in the docRef.id.isNotEmpty if statement right under the try-catch.
Update your CreateAccount function to this below:
Future CreateAccount() async {
try {
CollectionReference businessprofilescollectionref =
FirebaseFirestore.instance.collection('BusinessProfilesCollection');
DocumentReference docRef = await businessprofilescollectionref.add({
'Profile_city': bpaddSelectedcity,
'Profile_direct_category': directcategory,
});
} catch (e) {
showDialog(context: context, builder: (_) => FailureDialog(context));
return;
}
String documentid = docRef.id.toString();
print(documentid);
FirebaseStorage storage = FirebaseStorage.instance;
Reference ref = storage.ref().child("BusinessProfileImages/$documentid".toString());
UploadTask uploadTask = ref.putFile(bpaddimagefile);
final snapshot = await uploadTask.whenComplete(() {});
print("Image Uploaded");
final urldownload = await snapshot.ref.getDownloadURL();
print(urldownload);
businessprofilescollectionref.doc(documentid).update({'Profile_image_link': urldownload});
print("Link Updated");
await showDialog(context: context, builder: (_) => SuccessDialog(context));
Navigator.of(context, rootNavigator: true).pop();
}

Get multiple Image download URLs from Firebase Storage- I can successfully upload multiple images

I am trying to upload multiple images to firebase storage at once. I can upload successfully but cannot retrieve the download URLs;
Future<void> saveImages() async {
int index = images.length;
setState(() {
isLoading = true;
});
try {
final ref = FirebaseStorage.instance
.ref()
.child('images/${FirebaseAuth.instance.currentUser!.uid}');
images.forEach((element) async {
final file = await element.file;
final refPut = ref.child('$element$index');
await refPut.putFile(
File(file!.path),
);
});
final ref2 = ref.child('${imagePciker.path.split('/')}');
await ref2.putFile(File(imagePciker.path));
final ListResult listData = await ref.listAll();
final data =await ref2.getDownloadURL();
print('Im HERE!=Line 95');
Future.forEach(listData.items, (Reference element) async {
await element.getDownloadURL().then((value) => listUrls.add(value));
print('line 101');
print(listUrls.length);
});
print('Line 104');
await FirebaseFirestore.instance.collection('test').add({
'titleDownloadUrl': data,
'ListOfDownloadUrl': listUrls,
});
print('Line 108');
setState(() {
isLoading = false;
});
} catch (e) {
/* setState(() {
isLoading = false;
}); */
print(e);
}
}
The print statements are to debug. This whole function returns and no errors are thrown.
However in the firebase collection 'test' the URLs are not stored correctly.
On the first/fresh run after restart of the app.
the array of ListOfDownloadUrl is empty
On a hot restart(with the no images saved in firebase storage)
the array of ListOfDownloadUrl has one URL
then on multiple restarts, the amount of URLs saved becomes huge(ie. 4 images uploaded-10 URLs saved).
Please comment if I need to add anything else;
Thanks for any Help
.forEach() is NOT ASYNCHRONOUS - it WILL NOT wait. Use await Promise.all(images.map()) (thus creating an array of promises, which must all resolve).
Solved!
Following the thinking of #LeadDreamer
The forEach isn't ASYNCHRONOUS, the problem with his answer is that flutter does not have
Promise.all()
What it does have if Future.wait(), which acts(in my case) the same way.
Future<void> saveImages() async {
int index = images.length;
setState(() {
isLoading = true;
});
try {
final refMain = FirebaseStorage.instance
.ref()
.child('images${FirebaseAuth.instance.currentUser!.uid}');
AssetEntity eleId =
new AssetEntity(id: '', typeInt: 5, width: 5, height: 5);
await Future.wait(images.map((element) async { <-- here instead of the for each
setState(() {
eleId = element;
});
final file = await element.file;
final refPut = refMain.child('suportImage$_counter/$element$index');
await refPut.putFile(
File(file!.path),
);
}));
final ref2 =
refMain.child('headingImage$_counter/${imagePciker.path.split('/')}');
await ref2.putFile(File(imagePciker.path));
final ListResult listData =
await refMain.child('suportImage$_counter').listAll();
final data = await ref2.getDownloadURL();
await Future.wait(listData.items.map((e) async { <-- here instead of for each
await e.getDownloadURL().then((value) => listUrls.add(value));
}));

Async function doesn't await

I'm trying to upload an image to firebase storage but when I called the function, await is not executed to get the url. What am I missing in this?
Looking at this other topic I've got that the problem may be the "then", but how can I set the code to await the url?
Async/Await/then in Dart/Flutter
Future < String > uploadImage(File imageFile) async {
String _imageUrl;
StorageReference ref =
FirebaseStorage.instance.ref().child(firebaseUser.uid.toString());
await(ref.putFile(imageFile).onComplete.then((val) {
val.ref.getDownloadURL().then((val) {
_imageUrl = val;
print(val);
print("urlupload");
});
}));
print(_imageUrl);
print("urlnoupload");
return _imageUrl;
}
Thanks!
you had async/await when you didn't need it because you were capturing value in the then function
Future<String> uploadImage(File imageFile) async {
String _imageUrl;
StorageReference ref = FirebaseStorage.instance.ref().child(firebaseUser.uid.toString());
return ref.putFile(imageFile).onComplete.then((val) {
return val.ref.getDownloadURL()
}).then((_imageUrl) {
return _imageUrl;
});
},

How to wait for a value by using Await

I'm using Flutter to upload an image to Firebase and have a function submit() that is triggered when I submit the form. On submission, I validate that the submission is accurate and I call the uploadFile function to upload the designated image to Firebase storage and return the URL, which I set to urlForPost.
I want to wait for this urlForPost value to be set before triggering the remainder of the submit() function that uploads this to Firebase. Currently, it is returning a null value for urlForPost. How do I wait for the uploadFile() function to load so that I can prevent urlForPost from being null?
void submit() async {
// First validate form.
if (this._formKey.currentState.validate()) {
_formKey.currentState.save();// Save our form now.
final urlForPost = await uploadFile();
Firestore.instance
.collection('posts')
.document(documentName)
.collection('collection')
.add({
'user': widget.userPoster,
'post': _data.post,
'url': urlForPost,
'timePosted': Timestamp.now(),
});
Firestore.instance.collection('current_goals').document(widget.userPoster).collection(widget.goalType).document(widget.goalID).updateData(
{
'complete': true,
}
);
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => Home())); }
}
String downloadUrl;
Future<String> uploadFile() async {
final String rand1 = "${new Random().nextInt(10000)}";
final String rand2 = "${new Random().nextInt(10000)}";
final String rand3 = "${new Random().nextInt(10000)}";
final StorageReference ref = FirebaseStorage.instance.ref().child('${rand1}_${rand2}_${rand3}.jpg');
await ref.putFile(widget.selectedImage).onComplete.then((val) {
val.ref.getDownloadURL().then((val) {
print(val);
downloadUrl = val; //Val here is Already String
});
});
return downloadUrl;
}
You could change your uploadFile method to await for the upload as well.
You use await to make an asynchronous call synchronous. However, if you mix it with .then() methods, you're likely to make parts of it unintentionally asynchronous.
Future<String> uploadFile() async {
final String rand1 = "${new Random().nextInt(10000)}";
final String rand2 = "${new Random().nextInt(10000)}";
final String rand3 = "${new Random().nextInt(10000)}";
// you don't need {} if it's a simple statement, use $stringvar and ${value.toString}
final StorageReference ref = FirebaseStorage.instance.ref().child('$rand1_$rand2_$rand3.jpg');
StorageUploadTask task = ref.putFile(imageFile);
var downloadUrl = await (await task.onComplete).ref.getDownloadURL();
debugPrint("downloadUrl=$downloadUrl");
return downloadUrl.toString();
}
just a suggestion unrelated to your original question
Using 3 random numbers, you're likely to have a collision over time. Consider using the UUID package, the chances of collision are dramatically smaller. :)
Future<String> uploadFile() async {
final String uuid = uuid.v4(); // v4 is random, v1 is time-based
final StorageReference ref = FirebaseStorage.instance.ref().child('$uuid.jpg');
StorageUploadTask task = ref.putFile(imageFile);
var downloadUrl = await (await task.onComplete).ref.getDownloadURL();
return downloadUrl.toString();
}