Flutter - Dart : wait a forEach ends - firebase

I try to modify a string using data in each node I retrieve from Firebase database, and then to write a file with the modifed string (called "content").
Here is what I tried :
// Retrieve initial content from Firebase storage
var data = await FirebaseStorage.instance.ref().child("...").getData(1048576);
var content = new String.fromCharCodes(data);
// Edit content with each node from Firebase database
final response = await FirebaseDatabase.instance.reference().child('...').once();
response.value.forEach((jsonString) async {
...
// cacheManager.getFile returns a Future<File>
cacheManager.getFile(signedurl).then((file){
// Modify content
content=content.replaceAll('test',file.path);
});
});
// Finally write the file with content
print("test");
final localfile = File('index.html');
await localfile.writeAsString(content);
Result :
"test" is shown before the forEach ends.
I found that we can do in Dart (https://groups.google.com/a/dartlang.org/forum/#!topic/misc/GHm2cKUxUDU) :
await Future.forEach
but in my case if I do : await Future.response.value.forEach (sounds a bit weird)
then I get :
Getter not found: 'response'.
await Future.response.value.forEach((jsonString) async {
How to wait that forEach ends (with "content" modified) before to write the file with new content?
Any idea?

If you use for(... in ) instead of forEach you can use async/await
Future someMethod() async {
...
for(final jsonString in response.value) {
...
// cacheManager.getFile returns a Future<File>
cacheManager.getFile(signedurl).then((file){
// Modify content
content=content.replaceAll('test',file.path);
});
});
}
With forEach the calls fire away for each jsonString immediately and inside it await works, but forEach has return type void and that can't be awaited, only a Future can.

You defined the callback for forEach as async, which means that it runs asynchronously. In other words: the code inside of that callback runs independently of the code outside of the callback. That is exactly why print("test"); runs before the code inside of the callback.
The simplest solution is to move all code that needs information from within the callback into the callback. But there might also be a way to await all of the asynchronous callbacks, similar to how you already await the once call above it.
Update I got working what I think you want to do. With this JSON:
{
"data" : {
"key1" : {
"created" : "20181221T072221",
"name" : "I am key1"
},
"key2" : {
"created" : "20181221T072258",
"name" : "I am key 2"
},
"key3" : {
"created" : "20181221T072310",
"name" : "I am key 3"
}
},
"index" : {
"key1" : true,
"key3" : true
}
}
I can read the index, and then join the data with:
final ref = FirebaseDatabase.instance.reference().child("/53886373");
final index = await ref.child("index").once();
List<Future<DataSnapshot>> futures = [];
index.value.entries.forEach((json) async {
print(json);
futures.add(ref.child("data").child(json.key).once());
});
Future.wait(futures).then((List<DataSnapshot> responses) {
responses.forEach((snapshot) {
print(snapshot.value["name"]);
});
});

Have you tried:
File file = await cacheManager.getFile(signedurl);
content = content.replaceAll('test', file.path);
instead of:
cacheManager.getFile(signedurl).then((file){ ...});
EDIT:
Here's a fuller example trying to replicate what you have. I use a for loop instead of the forEach() method:
void main () async {
List<String> str = await getFuture();
print(str);
var foo;
for (var s in str) {
var b = await Future(() => s);
foo = b;
}
print('finish $foo');
}
getFuture() => Future(() => ['1', '2']);

Related

Flutter: How to loop through each field in `DocumentSnapshot.data()`

I have a collection where each user has their own document. In the document I create a map for a new entry. The document would look something like this:
In my app, I want to get the document and then turn it into a list where each map (titled by the time) would be an entry. I tried looping through it but the problem is that there is not a forEach() in the DocumentSnapshot.data() and it is a type of _JsonDocumentSnapshot.
How can I get this as a List where 2021-05-30 20:49:59.671705, 2021-05-30 20:50:00:600294, ... are individual elements in a list. I plan on building each entry on its own using something such as a ListView.builder() so I would like to have a list of all those entries.
Edit:
My code looks something like this:
journal is just a DocumentReference.
journal.snapshots().forEach((element) {
var data = element.data();
print(element.data());
(data ?? {}).forEach((key, value) {
return JournalEntryData(
date: key,
entryText: value['entryText'],
feeling: value['feeling']);
});
});
The problem was that the type of data which was set to element.data() was Object? hence the forEach() method wasn't defined. I tried casting the element.data() as a List which caused errors so I didn't go further down that path. What I should have done was to cast element.data() as a Map which worked. I also should have used journal.get() and not snapshots() as it is a stream so the await keyword would result in the journal.snapshots().forEach() to never end.
The final code looked something like this:
Future<List<JournalEntryData>> getJournalEntries() async {
List<JournalEntryData> entries = [];
await journal.get().then((document) {
Map data = (document.data() as Map);
data.forEach((key, value) {
print('adding entry');
entries.add(JournalEntryData(
date: key,
entryText: value['entryText'],
feeling: value['feeling'],
));
});
});
return entries;
}
and I would do the following to get the entries:
var entries = await getJournalEntries();
Hey for most of my problems like these I choose to go with this approach;
final result = await _db
.collection("collectionName")
.get();
List<Object> toReturn = [];
for (int i = 0; i < result.docs.length; i++) {
// add data to list you want to return.
toReturn.add();
}
Hopefully, this will be helpful to you.
List collectionElements = [];
void messagesStream() async {
await for (var snapshot in _firestore.collection('your collection').snapshots().where(
(snapshot) => snapshot.docs
.every((element) => element.author == currentUser.id),
)) {
collectionElements.add(snapshot);
}
print(collectionElements);
}
Each element would be added to the collectionElements list.
If you want to access only one field in your snapshot this works fine:
final entryText= element.data()['entryText'];
print(entryText); // this would print "lorem ipsum..."

ClientException, and i can't print the returned value (the request body)

Alright i'm losing my mind here,
in my flutter app, i'm using this function to perform post requests :
Future<Map> postRequest(String serviceName, Map<String, dynamic> data) async {
var responseBody = json.decode('{"data": "", "status": "NOK"}');
try {
http.Response response = await http.post(
_urlBase + '$_serverApi$serviceName',
body: jsonEncode(data),
);
if (response.statusCode == 200) {
responseBody = jsonDecode(response.body);
//
// If we receive a new token, let's save it
//
if (responseBody["status"] == "TOKEN") {
await _setMobileToken(responseBody["data"]);
// TODO: rerun the Post request
}
}
} catch (e) {
// An error was received
throw new Exception("POST ERROR");
}
return responseBody;
}
The problems are :
I get a ClientException (Not every time)
In another class, I stored the result of this function in a variable, it's supposed to return a Future<Map<dynamic, dynamic>>, when i printed it it shows :
I/flutter ( 9001): Instance of 'Future<Map<dynamic, dynamic>>'
But when i run the same post request directly (without using a function) it worked, and it shows the message that i was waiting for.
note: in both cases (function or not), in the server side it was the same thing.
this is the function where i used the post request:
void _confirm() {
if (_formKey.currentState.saveAndValidate()) {
print(_formKey.currentState.value);
var v = auth.postRequest("se_connecter", _formKey.currentState.value);
print(v);
} else {
print(_formKey.currentState.value);
print("validation failed");
}
}
Well for the second problem, i just did these changes:
void _confirm() async {
and
var v = await auth.postRequest('se_connecter', _formKey.currentState.value);
and yes it is stupid.
For the exception, it was the ssl encryption that caused it, so i removed it from my backend.

Rxdb Plugin: Using the RxCollectionBase#insert method in a plugin

I am trying to create a plugin for rxdb.
I want to catch the exception raised by insert and return an hash with
{[fieldName: string] => [error:string]}
When using my new method though, I am getting an exception, and it seems like the method is getting called directly on the prototype rather than on each RxColletion<T, T2, T3> instance.
The error i am getting is:
TypeError: Cannot read property 'fillObjectWithDefaults' of undefined
which happens here: https://github.com/pubkey/rxdb/blob/ac9fc95b0eda276110f371afca985f949275c3f1/src/rx-collection.ts#L443
because this.schema is undefined.. The collection I am running this method on does have a schema though..
Here is my plugin code:
export const validatedInsertPlugin: RxPlugin = {
rxdb: true,
prototypes: {
RxCollection(proto: IRxCollectionBaseWithValidatedInsert) {
proto.validatedInsert = async function validatedInsert<T, D>(
doc: T
): Promise<Insert<T>> {
try {
// this is the line that raises:
const product = await proto.insert(doc);
return [true, product];
} catch (e) {
// extract errors
return [false, {} as Errors<T>];
}
};
},
},
overwritable: {},
hooks: {},
};
To answer my own question,
proto.insert is targeting the prototype, which is not what I want.
function(this: RxCollection) is what I want. I have to use this which will target the actual instance.
proto.validatedInsert = async function validatedInsert<T1>(
this: RxCollection,
doc: T1
): Promise<ValidatedInsert<T1>> {
try {
const product = await this.insert(doc); // this, not proto
return [true, product];
} catch (e) {
...

In Meteor, how to choose a collection based on a variable?

Let's say you want to dynamically insert into different collections. Right now I am using a switch statement:
switch (i) {
case "dog":
Dog.insert({
name: "Skippy"
});
break;
case "cat":
Cat.insert({
name: "Skippy"
});
break;
}
But this is messy, and if I need to support future collections, it fails. Is there a way to choose the collection based on "i" in the example above?
Correct me if I am wrong but I think this is what you are trying to do:
var Dog = {
insert: function(props) {
console.log(props);
}
}
var insertArbitraryDocument = (function(collectionType, props) {
window[collectionType].insert(props)
}).bind(this);
insertArbitraryDocument('Dog', {name: 'skippy'}); //=> {name: 'skippy'}
In this snippet you are accessing the window object and getting the property of whatever name you are passing in (must be exactly the same as the collection). Then you can call your usual function calls.
I don't think there is a meteor built-in way of doing this, but it's pretty easy to just create a directory of collections manually:
JS in common to client and server:
var collections = {};
function myColl(name) {
var coll = new Meteor.Collection(name);
collections[name] = coll;
return coll;
}
// and now just use myColl instead of new Meteor.Collection
Dog = myColl('dog');
And then, to do what you want to do:
collections[i].insert(data);
Here's a complete working example:
Posts = new Mongo.Collection('posts');
Comments = new Mongo.Collection('comments');
var capitalize = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};
var nameToCollection = function(name) {
// pluralize and capitalize name, then find it on the global object
// 'post' -> global['Posts'] (server)
// 'post' -> window['Posts'] (client)
var root = Meteor.isClient ? window : global;
return root[capitalize(name) + 's'];
};
var insertSomething = function(name, data) {
var collection = nameToCollection(name);
collection.insert(data);
}
Meteor.startup(function() {
// ensure all old documents are removed
Posts.remove({});
Comments.remove({});
// insert some new documents
insertSomething('post', {message: 'this a post'});
insertSomething('comment', {message: 'this a comment'});
// check that it worked
console.log(Posts.findOne());
console.log(Comments.findOne());
});
Note this is nearly identical to this question but I simplified the answer for more generic use.

Firebase on(child_added) some field 'undefined'

I am working on a real time application and i am using firebase with pure html and javascript (not angularJS).
I am having a problem where i saved user's data to firebase with the given code by firebase :
var isNewUser = true;
ref.onAuth(function(authData) {
if (authData && isNewUser) {
authData['status'] = 'active';
authData['role'] = 'member';
ref.child("users").child(authData.uid).set(authData);
}
});
This will add the authData to the /users/ node. As you can see that i also appended some custom fields to the authData, status and role.
Now i am using this code to get the user's data from firebase and display them.
ref4.on("value", function(snapshot) {
var snapshotData = snapshot.val();
console.log('username: '+snapshotData.status);
});
If i use on('value'), the status get printed out on the console but if i do it this way,
ref4.on("child_added", function(snapshot) {
var snapshotData = snapshot.val();
console.log('status: '+snapshotData.status);
});
It is showing undefined for the status. May i know what's wrong and how to fix this problem. Thank you.
Since value is returning the path provided by ref4, and child_added is returning each child of that path, it's unlikely both are going to have a key status.
Consider this data structure:
{
"users": {
"brucelee": {
"status": "awesome"
},
"chucknorris": {
"status": "awesomerest"
}
}
}
If I now query for this according to your incomplete example:
var ref = new Firebase('https://<instance>firebaseio.com/users/brucelee');
ref.on('value', function(snap) {
// requests the brucelee record
console.log(snap.name(), ':', snap.val().status); // "brucelee: awesome"
});
ref.on('child_added', function(snap) {
// iterates children of the brucelee path (i.e. status)
console.log(snap.name(), ':', snap.val().status); // THROWS AN ERROR, because status is a string
});
So to do this on child_added with a data structure like this (and presumably somewhat like yours), it would look as follows:
ref.on('child_added', function(snap) {
// iterates children of the brucelee path (i.e. status)
console.log(snap.name(), ':', snap.val()); // "status: awesome"
});

Resources