Flutter: Parsing Firebase Realtime Database snapshot to list - flutter

I tried searching other questions but the only similar question to this had answers in JavaScript instead of Dart/Flutter. I'm trying to get a list from my Firebase Realtime Database into my app as a List<BaseModel>
So far from what I've searched in the net, I think the result of the DataSnapshot is a map that I could parse so I tried it out but got this error: List<dynamic>' is not a subtype of type 'Map<dynamic, dynamic>
My Code:
Future<List<BaseModel>> getList(
{DatabaseReference query, Models modelType}) async {
List<BaseModel> list = new List();
DataSnapshot snap = await query.once();
Map<String, dynamic> json = Map.from(snap.value);
json.forEach((key, value) {
list.add(BaseModel(model: modelType, key: key, snapshot: value));
});
return list;
}
The weird thing is, even if I try to parse a non-list model I also get the same error.
My database structure looks like this:
Update:
BaseModel:
abstract class BaseModel {
factory BaseModel({Models model, String key, Map map}) {
switch (model) {
case Models.MyModel:
return MyMod.fromSnapshot(key: key, map: map);
break;
default:
throw ("Not a valid model.");
}
}
}
MyModel:
MyModel.fromSnapshot({String key, Map map}) {
_id = key;
_title = map['title'];
}
My Firebase query is just the database reference with .child("Root")

I found a solution!
My new code:
Future<List<BaseModel>> getList({DatabaseReference query, Models modelType}) async {
List<BaseModel> list = new List();
DataSnapshot snap = await query.once();
List<dynamic> resultList = snap.value;
for(var i = 0; i < resultList.length; i++) {
Map<dynamic, dynamic> map = Map.from(resultList[i]);
list.add(BaseModel(model: modelType, key: i.toString(), snapshot: map));
}
return list;
}
This should work assuming you parse the values from your model's .fromMap(yourMap) constructor method. Something like _title = yourMap['key'];

I had a similar experience where the snapshot.value sometimes returned a List and sometimes returned a Map. I searched for a long time to get an answer with no luck but I came up with a workaround.
I suspected that the problem was being caused by using a record key with a value of zero so I added 100 to each key before I wrote it to the db and then subtracted it when I had read and was processing the records. The problem went away in that I then always got a Map returned.
I have since seen a reason given for this behaviour and it confirmed that the zero key value was the culprit but unfortunately I didn't save the link. I think it was on one of the Firebase blogs.
I think the 0 record returns a List and the ones with positive values return a Map.
Anyway, try the adding 100 trick and see it that helps. if it helps, upvote me....I don't think you need code to add or delete 100. :-)
Found the article, Firebase is deciding if it should render an array or a map based on the snapshot content: https://firebase.googleblog.com/2014/04/best-practices-arrays-in-firebase.html?m=1
UPDATE:
My 'starting at 0' theory was a red herring, sorry.
The key to this behaviour (bits in bold) is in the part of the Firebase blog (link above) that states:
However, to help people that are storing arrays in Firebase, when you
call .val() or use the REST api to read data, if the data looks like
an array, Firebase will render it as an array.
In particular, if all of the keys are integers, and more than half of
the keys between 0 and the maximum key in the object have non-empty
values, then Firebase will render it as an array. This latter part is
important to keep in mind.
// we send this ['a', 'b', 'c', 'd', 'e'] // Firebase stores this {0:
'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e'}
// since the keys are numeric and sequential, // if we query the data,
we get this ['a', 'b', 'c', 'd', 'e']
// however, if we then delete a, b, and d, // they are no longer
mostly sequential, so // we do not get back an array {2: 'c', 4: 'e'}
You can't currently change or prevent this behavior.
I have now tested this by setting up a db node that looks like the below image. I tested what was being returned in snapshot.value by using snapshot.value is List and snapshot.value is Map, both return true or false.
When ONLY nodes 0, 1 and 3 were there, Firebase RTDB was happily returning a List in snapshot.value. When I added nodes 4 and 6, which have inconsistent data, it decided it was time to return a Map. :-)
So, the quick fix is to test the contents of snapshot.value with is List and/or is Map and then process the contents accordingly, otherwise rethink your keys...the fact that they are sequential or close-to-sequential (with gaps) but have the same children structure is the issue.

Related

How to update "key" name in hive flutter instead of updating value

i want to Update the "key" in the flutter hive. but it is updating the value in a flutter.
updateItems(int index, String value){
final listData=Hive.box(CommonKeys.DB_NAME);
listData.putAt(index,value);
final keys=listData.keys;
I assume you want to change the key of element at an index.
Anwser is you can't do that directly (work around is really complicated, tell me if you really need it).
And you shouldn't do that, hive is a key-value pair database which mean it designed for read/write value using key or index. So you can't edit key or index, but you can assign that value to a new key.
In your case, I recommend you to store as List or Object instead
updateItems(int index, String newKey) {
final listData = Hive.box(CommonKeys.DB_NAME);
// This list has only 2 elements, key and value;
final List keyValuePair = listData.getAt(index);
keyValuePair[0] = newKey;
listData.putAt(index, keyValuePair);
}

How do I only return my primary keys from a Firebase RTDB and not the rest of the data stored when using a get() in Flutter?

My menu item tree looks is shown below:
menuItem
J1
-description:"Tasty milk shake!"
-img:"assets/images/milkshake.JPG"
-itemName:"Milk Shake"
-price:20
-varieties
-var1:"Chocolate"
-var2:"Vanilla"
-var3:"Strawberry"
I want to get just the item IDs (J1, J2, J3 ect.) but not all the information such as 'itemName'
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref();
final items = await _dbRef.child('menuItem').get();
if (items.exists) {
String? itemID = items.value.toString();
}
items.values returns all the information for an item and items.key returns only 'menuItem'.
How can I just get the IDs only?
With the Realtime Database queries done via the Client SDKs are deep: They always return the entire subtree.
This is a key difference with Firestore for which queries are shallow: They only return documents in a particular collection or collection group and do not return subcollection data.
However, with the Realtime Database REST API you can use a query parameter named shallow, which "limits the depth of the data returned at a location". I've never used it but it seems that it will fulfill your requirement.
Another solution would to denormalise your data and maintain, in parallel to the menu items, a list of menu IDs in a specific DB node.
As Renaud explained in his answer, all read operations in the Firebase Realtime Database SDKs return complete branches of the tree, and can't be used to just read the keys.
That said, you can use just the keys from the data you read with:
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref();
final items = await _dbRef.child('menuItem').get();
items.forEach((child) => {
console.log(child.key);
})
The above will still retrieve the entire menuItem branch of your database, but only show the keys under that node (so J1 from the sample you shared).

Read nested data from firebase realtime database in flutter

If you see the screenshot of my database, the data is stored in a nested way (cartProduct is a key which has a value of an entire Json file with keys: "id", "price" etc.). In my code, I create a map of each record in the "Orders" table in order to retrieve key values of any key that I specify. This is done by specifying the key name in the databaseMapper variable.
I am trying to read the value of each "id" and store it in a list called "testerList". I am able to store each orderNum, totalAmount or any of those key values that I specify in the databaseMapper. However, if I specify ["id"] it does not work.
I did some research and saw that the only way to reference nested items in a map is by using the logic: databaseMapper["cartProduct"]["id"] like I did below, but I keep getting an error (see last screenshot).
Any help would be appreciated!
Future _readItemIDsTest() async {
//Stores each record in the table as a map
var snapshot = await _dbRef.child("Orders").get();
snapshot.children.forEach((childSnapshot) {
var databaseMapper = childSnapshot.value as Map;
testerList.addAll([databaseMapper["cartProduct"]["id"]]);
});
print(testerList);
}
Nvm, I figured it out. The code below solved my issue.
Future _readItemIDsTest() async {
//Stores each record in the table as a map
//Adds the itemName value of each item from the map
var snapshot = await _dbRef.child("Orders").get();
snapshot.children.forEach((childSnapshot) {
databaseMapper = childSnapshot.value as Map;
var cartProductList = databaseMapper["cartProduct"];
(cartProductList as List).forEach((cartProductElement) {
testerList.addAll([cartProductElement["id"]]);
});
});
print(testerList);
}

How to insert jsonb[] data into column using pg-promise

Given a table with a column of type jsonb[], how do I insert a json array into the column?
Using the provided formatters :array, :json won't work in this instance - unless I am missing the correct combination or something.
const links = [
{
title: 'IMDB',
url: 'https://www.imdb.com/title/tt0076759'
},
{
title: 'Rotten Tomatoes',
url: 'https://www.rottentomatoes.com/m/star_wars'
}
];
const result = await db.none(`INSERT INTO tests (links) VALUES ($1:json)`, [links]);
You do not need the library's :json filter in this case, as you need an array of JSON objects, and not a JSON with an array of JSON objects.
The former is formatted correctly by default, which then only needs ::json[] type casting:
await db.none(`INSERT INTO tests(links) VALUES($1::json[])`, [links]);
Other Notes
Use pg-monitor or event query to output queries being executed, for easier diagnostics.
Method none can only resolve with null, no point storing the result in a variable.
Library pg-promise does not have any :array filter, see supported filters.

Querying in Firebase by child of child

I have a structure of objects in Firebase looking like this:
-KBP27k4iOTT2m873xSE
categories
Geography: true
Oceania: true
correctanswer: "Yaren (de facto)"
languages: "English"
question: "Nauru"
questiontype: "Text"
wronganswer1: "Majuro"
wronganswer2: "Mata-Utu"
wronganswer3: "Suva"
I'm trying to find objects by categories, so for instance I want all objects which has the category set to "Oceania".
I'm using Swift and I can't really seem to grasp the concept of how to query the data.
My query right now looks like this:
ref.queryEqualToValue("", childKey: "categories").queryOrderedByChild("Oceania")
Where ref is the reference to Firebase in that specific path.
However whatever I've tried I keep getting ALL data returned instead of the objects with category Oceania only.
My data is structured like this: baseurl/questions/
As you can see in the object example one question can have multiple categories added, so from what I've understood it's best to have a reference to the categories inside your objects.
I could change my structure to baseurl/questions/oceania/uniqueids/, but then I would get multiple entries covering the same data, but with different uniqueid, because the question would be present under both the categories oceania and geography.
By using the structure baseurl/questions/oceania/ and baseurl/questions/geography I could also just add unique ids under oceania and geography that points to a specific unique id inside baseurl/questions/uniqueids instead, but that would mean I'd have to keep track of a lot of references. Making a relations table so to speak.
I wonder if that's the way to go or? Should I restructure my data? The app isn't in production yet, so it's possible to restructure the data completely with no bigger consequences, other than I'd have to rewrite my code, that pushes data to Firebase.
Let me know, if all of this doesn't make sense and sorry for the wall of text :-)
Adding some additional code to Tim's answer for future reference.
Just use a deep query. The parent object key is not what is queried so it's 'ignored'. It doesn't matter whether it's a key generated by autoId or a dinosaur name - the query is on the child objects and the parent (key) is returned in snapshot.key.
Based on your Firebase structure, this will retrieve each child nodes where Oceania is true, one at a time:
let questionsRef = Firebase(url:"https://baseurl/questions")
questionsRef.queryOrderedByChild("categories/Oceania").queryEqualToValue(true)
.observeEventType(.ChildAdded, withBlock: { snapshot in
print(snapshot)
})
Edit: A question came up about loading all of the values at once (.value) instead of one at at time (.childAdded)
let questionsRef = Firebase(url:"https://baseurl/questions")
questionsRef.queryOrderedByChild("categories/Oceania").queryEqualToValue(true)
.observeSingleEventOfType(.Value, withBlock: { snapshot in
print(snapshot)
})
Results in (my Firebase structure is a little different but you get the idea) uid_1 did not have Oceania = true so it was omitted from the query
results.
Snap (users) {
"uid_0" = {
categories = {
Oceania = 1;
};
email = "dude#thing.com";
"first_name" = Bill;
};
"uid_2" = {
categories = {
Oceania = 1;
};
"first_name" = Peter;
};
}
I think this should work:
ref.queryOrderedByChild("categories/Oceania").queryEqualToValue(true)