I have a Future which returns a map. I then need to use the values of that map to await another future and then return the entire result at the end. The problem is that dart can't await async Map.forEach() methods (see this: https://stackoverflow.com/a/42467822/15782390).
Here is my code:
the debug console shows that the items printed are in the following order:
flutter: getting journal entries
flutter: about to loop through pictures
flutter: getting picture
flutter: returning entries
flutter: [[....]] (Uint8List)
Future<List<JournalEntryData>> getJournalEntries() async {
List<JournalEntryData> entries = [];
print('getting journal entries');
EncryptService encryptService = EncryptService(uid);
await journal.get().then((document) {
Map data = (document.data() as Map);
print('about to loop through pictures');
data.forEach((key, value) async {
print('getting picture');
dynamic pictures = await StorageService(uid).getPictures(key);
print('done getting image');
entries.add(JournalEntryData(
date: key,
entryText: encryptService.decrypt(value['entryText']),
feeling: value['feeling'],
pictures: pictures,
));
});
});
print('returning entries');
return entries;
}
Future getPictures(String entryID) async {
try {
final ref = storage.ref(uid).child(entryID);
List<Uint8List> pictures = [];
await ref.listAll().then((result) async {
for (var picReference in result.items) {
Uint8List? pic = await ref.child(picReference.name).getData();
if (pic == null) {
// TODO make no picture found picture
var url = Uri.parse(
'https://www.salonlfc.com/wp-content/uploads/2018/01/image-not-found-scaled-1150x647.png');
var response = await http.get(url);
pic = response.bodyBytes;
}
pictures.add(pic);
}
});
return pictures;
} catch (e) {
print(e.toString());
return e;
}
}
It's quite annoying to have to use for-loops when you need async behaviour, specially on Maps, because as the other answer shows, that requires you to iterate over entries and then take the key and value out of it like this:
for (final mapEntry in data.entries) {
final key = mapEntry.key;
final value = mapEntry.value;
...
}
Instead of that, you can write a utility extension that does the work for you:
extension AsyncMap<K, V> on Map<K, V> {
Future<void> forEachAsync(FutureOr<void> Function(K, V) fun) async {
for (var value in entries) {
final k = value.key;
final v = value.value;
await fun(k, v);
}
}
}
Then, you can use that like this:
await data.forEachAsync((key, value) async {
...
});
Much better.
Don't mix the use of then and await since it get rather confusing and things are no longer being executed as you think.
Also, the use of forEach method should really not be used for complicated logic like what you are doing. Instead, use the for-each loop. I have tried rewrite getJournalEntries here:
Future<List<JournalEntryData>> getJournalEntries() async {
List<JournalEntryData> entries = [];
print('getting journal entries');
EncryptService encryptService = EncryptService(uid);
final document = await journal.get();
Map data = (document.data() as Map);
print('about to loop through pictures');
for (final mapEntry in data.entries) {
final key = mapEntry.key;
final value = mapEntry.value;
print('getting picture');
dynamic pictures = await StorageService(uid).getPictures(key);
print('done getting image');
entries.add(JournalEntryData(
date: key,
entryText: encryptService.decrypt(value['entryText']),
feeling: value['feeling'],
pictures: pictures,
));
}
print('returning entries');
return entries;
}
And getPictures here. I have only removed the use of then here.
Future getPictures(String entryID) async {
try {
final ref = storage.ref(uid).child(entryID);
List<Uint8List> pictures = [];
final result = await ref.listAll();
for (var picReference in result.items) {
Uint8List? pic = await ref.child(picReference.name).getData();
if (pic == null) {
// TODO make no picture found picture
var url = Uri.parse(
'https://www.salonlfc.com/wp-content/uploads/2018/01/image-not-found-scaled-1150x647.png');
var response = await http.get(url);
pic = response.bodyBytes;
}
pictures.add(pic);
}
return pictures;
} catch (e) {
print(e.toString());
return e;
}
}
Related
I have created a function to work on my app. This function add's the photo from my camera or gallery into the Firebase storage, and into the user collection. Althought I'm receiving a strange error when trying to add the data. I have attempted to pass throught this Exception but the data wasn't added neither.
The erro:
This is the function:
class Product {
final Firestore firestore = Firestore.instance;
final FirebaseStorage storage = FirebaseStorage.instance;
DocumentReference get firestoreRef => firestore.document('products/$id');
StorageReference get storageRef => storage.ref().child('products').child(id);
Future<void> save() async {
loading = true;
final Map<String, dynamic> data = {
'name': name,
'description': description,
'sizes': exportSizeList()
};
if (id == null) {
final doc = await firestore.collection('products').add(data);
id = doc.documentID;
} else {
await firestoreRef.updateData(data);
}
final List<String> updateImages = [];
for (final newImage in newImages!) {
if (images.contains(newImage)) {
updateImages.add(newImage as String);
} else {
final StorageUploadTask task =
storageRef.child(Uuid().v1()).putFile(newImage as File);
final StorageTaskSnapshot snapshot = await task.onComplete;
final String url = await snapshot.ref.getDownloadURL() as String;
updateImages.add(url);
}
}
for (final image in images) {
if (!newImages!.contains(image)) {
try {
final ref = await storage.getReferenceFromUrl(image);
await ref.delete();
} catch (e) {
debugPrint('Falha ao deletar $image');
}
}
}
await firestoreRef.updateData({'images': updateImages});
images = updateImages;
loading = false;
}
}
From the error message is looks like id doesn't have a value in this call:
firestore.document('products/$id');
When id has no value, that leads to a document reference with a path /products/, which explains the error message.
So you'll want to run the code in a debugger, set a breakpoint on that line, and figure out why id doesn't have a value at that point.
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));
}));
I have a String List in my shared preferences that's meant to store account numbers which can then be used to make a call from the API:
List<String> accountList = [];
Future _getListData() async {
SharedPreferences myPrefs = await SharedPreferences.getInstance();
for(var i = 0; i < myPrefs.getStringList('accounts').length; i++){
accountList.add(myPrefs.getStringList('accounts')[i]);
}
//prints values from shared preferences
print(accountList);
}
Future<List<dynamic>> fetchData() async {
_getListData();
//prints an empty list
print(accountList);
try {
if (accountList == null) {
var result = await http.get(apiUrl);
List<dynamic> accountInfo = (json.decode(result.body));
return accountInfo;
} else {
List<dynamic> accountInfo = [];
for(var i = 0; i < accountList.length; i++){
var result =
await http.get(apiUrl + "/api/Data/GetCustomer?accntnum=" + accountList[i]);
accountInfo.add(json.decode(result.body));
}
return accountInfo;
}
} catch (e) {
print(e);
}
}
When calling the Future function "fetchData()" from my FutureBuilder in the widget it returns an empty list. However within the scope of the "_getListData()" function it prints the list witht he appropriate values. How can I make it such that "fetchData()" gets the intended list?
Just add an await before _getListData() here:
Future<List<dynamic>> fetchData() async {
await _getListData();
//prints an empty list
print(accountList);
I am experiencing this weird behavior when loading assets from external storage, sometimes the path gets added to the list and most of the time the path is not added.
Here is my function, Am I missing something?
Future<List<String>> loadAssets() async {
List<String> loadedAssets = [];
loadedAssets.add('test');
try {
final Directory dir = await syspath.getExternalStorageDirectory();
dummyData.forEach((path) async {
final extPath =
path.substring(('assets/products_dummy_data/'.length));
final localPath='${dir.path}/$extPath}';
final file = File(localPath);
if (await file.exists()) {
await file.delete();
}
final data = await rootBundle.load(path);
var asUint8List =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await file.create(
recursive: true,
);
await file.writeAsBytes(asUint8List);
if (await file.exists()) {
loadedAssets.add(localPath);
}
});
} catch (e, s) {
AppHelper.appLogger.e('Error while loading assets', e, s);
}
AppHelper.appLogger.i('loadedAssets.length ${loadedAssets.length}');
return loadedAssets;
}
But I always get the length as one, for the test element added
Problem Fixed after using await Future.forEach
I'm trying to call a cloud firestore function that has to persist an object only when another function returns the url of the file in Firebase Storage but the async await is not working and the second function is called anyway whereas the first function is not yet completed!!!
await schoolProfileProvider.uploadSchoolProfileAvatar(data).then( (data) {
schoolProfileProvider.addSchoolProfile(data);
});
print('PROFILE ADDED');
Future<SchoolProfileData> uploadSchoolProfileAvatar(SchoolProfileData data) async {
List<File> avatars = [];
data.childrenDetails.forEach((child) {
avatars.add(File(child.childImage));
});
try {
await _api.uploadFilesToStorage(avatars, 'image', 'png').then((urls) {
for (var i = 0; i < urls.length; i++) {
data.childrenDetails[i].childImage = urls[i];
print('ADD ' + data.childrenDetails[i].childImage);
}
});
} on Exception catch (e) {
print(e.toString());
}
return data;
}
T cast<T>(x) => x is T ? x : null;
Future<List<String>> uploadFilesToStorage(List<File> files, String type, String extension) async {
final urls = <Future<String>>[];
files.forEach((file) async {
StorageReference storageRef = storage.ref().child(file.path);
final StorageTaskSnapshot downloadUrl =
(await storageRef
.putFile(file, StorageMetadata(contentType: type + '/' + extension))
.onComplete);
await downloadUrl.ref.getDownloadURL().then((url) {
urls.add(cast<Future<String>>(url));
print('URL for file ${file.path} = ${url.toString()}');
});
});
print ('urls returned');
return Future.wait(urls);
}
Future addSchoolProfile(SchoolProfileData data) async{
var result;
try {
result = await _api.addDocument(data.toJson());
} on Exception catch(e) {
print (e.toString());
}
return result;
}
I've managed to make the things work and execute addSchoolProfile only after uploadFilesToStorage is completed by reducing the nested functions and making the await downloadUrl.ref.getDownloadURL() as the last instruction returned in the callee function.
Please find the code for who is interested in :
The caller :
schoolProfileProvider.addSchoolProfileAndUploadAvatar(data);
Future addSchoolProfileAndUploadAvatar(SchoolProfileData data) async {
List<File> avatars = [];
data.childrenDetails.forEach((child) {
avatars.add(File(child.childImage));
});
try {
for (int i=0;i<avatars.length;i++){
await _api.uploadFileToStorageAndGetUrl(avatars[i], 'image', 'png').then((url) {
print('URL for file ${avatars[i].path} = ${url.toString()}');
data.childrenDetails[i].childImage = url;
print('ADD ' + data.childrenDetails[i].childImage);
});
}
_api.addDocument(data.toJson()) ; // Add document in Google Firebase after setting the avatar url
} on Exception catch (e) {
print(e.toString());
}
}
The callee :
Future <String> uploadFileToStorageAndGetUrl(File file,String type, String extension) async{
StorageReference storageRef = storage.ref().child(file.path);
final StorageTaskSnapshot downloadUrl =
(await storageRef
.putFile(file, StorageMetadata(contentType: type + '/' + extension))
.onComplete);
return await downloadUrl.ref.getDownloadURL(); // return the url of the file in Google Storage
}