I am trying to load all of my product data from firestore. I have a data schema:
class SingleProduct with ChangeNotifier {
static const EVENT = "event";
static const IMGURL = "imgUrl";
static const NAME = "name";
static const PRICE = "price";
//Private Variables
String _event;
String _imgUrl;
String _name;
double _price;
// getters
String get event => _event;
String get imgUrl => _imgUrl;
String get name => _name;
double get price => _price;
SingleProduct.fromSnapshot(DocumentSnapshot snapshot) {
_event = snapshot.data()[EVENT];
_imgUrl = snapshot.data()[IMGURL];
_name = snapshot.data()[NAME];
_price = snapshot.data()[PRICE];}
}
I have then created a class to map all the data received to a list of products:
class ProductServices {
FirebaseFirestore _firestore = FirebaseFirestore.instance;
String collection = 'products';
Future<List<SingleProduct>> getAllProducts() async =>
_firestore.collection(collection).get().then((snap) {
print(snap.docs.length); // returns 11 products as expected
List<SingleProduct> allProducts = [];
snap.docs.map((snapshot) =>
allProducts.add(SingleProduct.fromSnapshot(snapshot)));
print(allProducts.length); //returns 0 so I think my map isn't working
return allProducts;
});
}
The Firestore query returns 11 query snapshots as expected but I then try and add them to a list using my product schema but the the results show the list has 0 elements. Any suggestions how to map the results of my fire base query to a list?
Use forEach():
snap.docs.forEach((snapshot) => allProducts.add(SingleProduct.fromSnapshot(snapshot)));
Explanation time: [tl;dr Your lambda wasn't executed at all]
map() is a lazy function intended for transforming elements in a list, which works only if the result is going to be used.
In your code, the result of map() is nowhere used later (eg. assigned to a variable), which is why the lambda within it is not called (why would it transform if the transformation is not used ahead?)
Also, it's not apt for your use-case.
To demonstrate its laziness, try running this code in DartPad:
void main() {
List<String> list = ["a", "b", "c"];
List<String> anotherList = [];
var mappingOutput = list.map((element) { anotherList.add(element); return element + "X"; }).toList();
print(list);
print(mappingOutput);
print(anotherList);
}
Notice that the result of map() is to be given back to a variable mandatorily, which pushes the laziness aside and executes it.
anotherList will be filled.
Related
I am trying to make a multivendor app with Flutter.
The problem I am facing right now is how can I make a model class within a Model class:
Example:
class ProductModel {
String? name;
double? price;
String? imgUrl;
double? weight;
ProductModel();
ProductMode.fromSnapShot(DocumentSnapshot snapshot){
name = snapshot['name'];
price= snapshot['price'];
imgUrl= snapshot['imgUrl'];
weight= snapshot['weight'];
}
}
I have already retrieved all the products from Firestore:
class VendorModel{
String? name;
String? imgUrl;
DateTime? opensAt;
DateTime? closesAt;
VendorModel();
VendorModel.fromSnapshot(DocumentSnapshot snapshot){
name = snapshot['name'];
imgUrl= snapshot['imgUrl'];
opensAt= DateTime.tryParse(snapshot['opensAt'].toString());
closesAt= DateTime.tryParse(snapshot['closesAt'].toString());
}
}
I have also retrieved all the vendors from Firestore:
Here is What I am trying to do:
I would like to add the products I fetched from Firestore to their respective vendors using this model. But I don't know how to make this if my data is from Firestore.
class VendorWithProductsModel{
VendorModel vendor;
List<ProductModel> products;
VendorWithProductsModel();
(I AM STUCK HERE)
}
Here is a Snippet of my Firestore code in retrieving products and vendors:
VendorModel _vendor = VendorModel();
List<VendorModel> _allVendors = [];
Future<VendorModel> getCertainVendor(docId) async{
await vendor.doc(docId).get().then((value){
_vendor = VendorModel.fromSnapshot(value);
});
return _vendor;
}
Future<List<VendorModel>> getAllVendors() async{
await vendor.get().then((value){
_allVendors.add(VendorModel.fromSnapshot(value));
});
return _allVendors;
}
In your product model, you need something like vendorId, where you can know which product belongs to which vendor.
After fetching all your products and vendors. Use both list, and loop through them, this is pseudo code for example:
List<VendorWithProductsModel> doMagic (List<ProductModel> productList, List<VendorModel> vendorList) {
List<VendorWithProductsModel> vendorsWithProducts =[];
vendorList.forEach((vendor){
VendorWithProductsModel _vendorAndProduct = VendorWithProductsModel();
_vendorAndProduct.vendor = vendor;
_vendorAndProduct.products =[];
_vendorAndProduct.products = productList.where((e)=> e.vendorId == vendor.id).toList();
vendorsWithProducts.add(_vendorAndProduct);
});
return vendorsWithProducts;
}
I am trying to create a method to convert a List to Future<List>.
This is the method I created.
static Future<List<Product?>> fromProductRefList(
List<DocumentReference> ref) async {
List<Product> shopProductList = [];
ref.forEach((productRef) async {
final productDoc = productRef.get();
final product = await Product.fromDocument(await productDoc);
shopProductList.add(product!);
});
print('shopProductList: $shopProductList');
return shopProductList;
}
and called it in cubit,
void mapProductToState() async {
emit(state.copyWith(status: MyProductStatus.loadding));
final shop = _shopBloc.state.shop;
List<Product?> productList = [];
if (shop.shopProductRef.isNotEmpty) {
final productList = Product.fromProductRefList(shop.shopProductRef);
}
emit(state.copyWith(
shop: shop,
productList: productList,
status: MyProductStatus.loaded,
));
}
VScode shows no error but when I run the code, fromProductRefList return empty list. Seems like fromProductRefList did not wait for the Document actually get() from the database and just return.
When I add a second delay in fromProductRefList before returning the shopProductList, everything works as expected.
I have read another question on stackoverflow suggest using asyncMap() but I am not sure how to apply it in my case.
Edit:
When I add a delay, the method return without any issue. If not, it will return a empty list
static Future<List<Product?>> fromProductRefList(
List<DocumentReference> ref) async {
List<Product> shopProductList = [];
ref.forEach((productRef) async {
final productDoc = productRef.get();
final product = await Product.fromDocument(await productDoc);
shopProductList.add(product!);
});
await Future.delayed(const Duration(milliseconds: 500));
print('shopProductList: $shopProductList');
return shopProductList;
}
Thank you.
You are missing the await keyword, to actually wait for the call. It only compiles, because you also declare a new variable of name productList, shadowing the already existing one.
So this line:
final productList = Product.fromProductRefList(shop.shopProductRef);
should read:
productList = await Product.fromProductRefList(shop.shopProductRef);
In addition, this does not do what you think it does:
ref.forEach((productRef) async {
It does not wait for each call. Please use a normal for flow control structure and await the async call, not the forEach method. The forEach method will no wait for the Futures returned from the methods.
let say I store a user document in Firestore, each user document has name (string) and age (number) as its field.
I can parse the data from Firestore using User.fromFirestore() below
class User {
final String name;
final int age;
User({
required this.name,
required this.age,
});
factory User.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return User(
name: data["name"] ?? "",
age: data["age"] ?? 0,
);
}
}
as you can see above in User.fromFirestore constructor.....
if for example the name field is deleted in the server, then my code above can handle it by assigning an empty string using null coalescing operator ( ?? )
but what if the data type of a field is changed accidentally ?
for example, the age should be a number in server, but if the age accidentally become string (let say the age field now has string value "twenty two") then my app will crash if I use my code above, right?
because data["age"] is not null but it is a string now, but my User model requires int for age property.
what should I do to handle this?
it is not limited from number to string actually. it can be from number to Geopoint, to List, to Timestamp etc. I need my Flutter app more fault tolerant when parsing Firestore data into Dart object
You could check the type of data you're getting before doing assigning it to the properties of the User object.
So this code block below:
return User(
name: data["name"] ?? "",
age: data["age"] ?? 0,
);
becomes this:
return User(
name: data["name"] is String ? data["name"] : "",
age: data["age"] is int ? data["age"] : 0,
);
The code checks for the data type and if it is the type it expects, it gets the data from the data Map. And if not it assigns the default value.
And this code also handles situations when the data is null. This is because null is of type Null and so it will fail the type check and the default value will be assigned to the variable.
If you have classes with a lot of properties, you can abstract the logic into helper methods do the type check.
Check out the code below:
class User {
final String name;
final int age;
User({
#required this.name,
#required this.age,
});
static int getIntOrDefault(dynamic value) {
return value is int ? value : 0;
}
static String getStringOrDefault(dynamic value) {
return value is String ? value : '';
}
factory User.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return User(
name: getStringOrDefault(data["name"]),
age: getIntOrDefault(
data["age"],
),
);
}
}
Simple, just make your types dynamic or String.
You can easily handle Strings because all types easily convert into string.
Now for the parsing part, wherever you are trying to use the age except for display purposes (i.e., Text(age.toString())), you can actually try if parsing is returning a valid number
final String age;
...
try {
int ageInNum = int.parse(age);
return Text((ageInNum + 3).toString()); // If you are doing this for some reason.
} catch (e) {
// Handle your code gracefully here so that you disable the operation you are trying to perform
return Text('Unable to perform action');
}
// If you want to do it in the constructor itself
factory User.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, String>; // Change the value type to `String`
int age = 0; // your default case
try {
age = int.parse(data["age"]);
} catch (_) {}
return User(
name: data["name"] ?? "",
age: age,
);
}
This should work.
The code and comments kind of explains it better than I can articulate it.
basically I want to return a Stream of something but only based on certain parameters. Those parameters are coming from an array.
here is an example.
say we have an array with values ["1", "2", "3"]
and in the database I have a docids of ["1", "2","3","4"]
I want a stream that will return everything but that four, or to better articulate it. I want a stream list that will return only the items that have the docid of the array with those values specified aka [1,2,3]
what I did below was loop through the example array so the first item "c" will be "1".
it will take this "1" and use a where to see if a docid matches this "1". I need to store this somehow and then return it once it is "fullly" populated. or populated at all since it is a stream. The example array of [1,2,3] could change in the future to maybe [1,2,3,4] so when that happens I would like the data to be pulled from the database.
class UserData {
String uid;
String firstName;
int rating;
List<String> classes; //need to be able to access this
UserData.fromMap(Map<String, dynamic> data) {
firstName = data['firstname'] ?? "";
rating = data['rating'] ?? "";
classes = data['classes'].cast<String>() ?? "";
}
Stream<List<ClassData>> getTheUserClasses = (() async* {
List<ClassData> d = List<ClassData>();
for (String c in classes) { //no longer able to access classes
// this will store the data of the matched value
var _matchData = Firestore.instance
.collection("Classes")
.where('classid', isEqualTo: c)
.snapshots()
.map((event) => event.documents
.map((e) => ClassData.fromUserMap(e.data, e.documentID)));
// and when you have it, append
d.add(_matchData); //error here from type differences
}
// after the loop you can test your response then yield
d.forEach((item) => print(item));
// return the data which is required
yield d;
})();
UserData({this.firstName, this.rating, this.classes});
}
Here is a way I have already done this. The problem is that it won't refresh the widget tree when data is updated.
Future<void> getTheUserClasses() async {
List<ClassData> _classList = List<ClassData>();
for (String c in user.classes) {
DocumentSnapshot classsnapshot =
await Firestore.instance.collection("Classes").document(c).get();
final data =
ClassData.fromUserMap(classsnapshot.data, classsnapshot.documentID);
if (data != null) {
_classList.add(data);
}
}
notifyListeners();
classes = _classList;
}
Hi I want to deserialise the snapshot from the realtime database to a Company object and add it to a _companies list.
But I keep getting an error...
This is what I have so far:
List<Company> _companies = [];
#override
void initState() {
// TODO: implement initState
super.initState();
getItems().then((list){
print("Now the list is here");
setState(() {
for (int i=0; i < list.length; i++) {
Map<String, dynamic> map = list[i];
Company company = new Company.fromMap(map);
_companies.add(company);
}
});
});
}
static Future<List> getItems( ) async {
Completer<List> completer = new Completer<List>();
FirebaseDatabase.instance
.reference()
.child("Companies")
.once()
.then( (DataSnapshot snapshot) {
List map = snapshot.value; //It fails here
completer.complete(map);
} );
return completer.future;
}
This is my Company class:
class Company {
String key;
String category;
String company_name;
String company_url;
String country;
String description;
String email;
String faq_url;
String instagram_url;
String logo_url_image;
String p_category;
String parent_company_ok;
String timestamp;
Company();
Company.fromSnapshot(DataSnapshot snapshot)
: key = snapshot.key,
category = snapshot.value['category'],
company_name = snapshot.value['company_name'],
company_url = snapshot.value['company_url'],
country = snapshot.value['country'],
description= snapshot.value['description'],
email = snapshot.value['email'],
faq_url = snapshot.value['faq_url'],
instagram_url = snapshot.value['instagram_url'],
logo_url_image = snapshot.value['logo_url_image'],
p_category = snapshot.value['p_category'],
parent_company_ok = snapshot.value['parent_company_ok'],
timestamp = snapshot.value['timestamp'];
}
How ever, it fails in getItems( ) on List map = snapshot.value;. With the exeption: _InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'List<dynamic>'
Also if anyone can provide the code for showing the children in a List widget I would be very happy :)
Here is my data structure in firebase realtime database:
Well as the error message says snapshot.value is not a List but it is a map so the line List map = snapshot.value; will always fail. Since you are reading Companies node tree from your database this will return a map of maps, something like Map<String, Map<dynamic,dynamic>> with all data from respective node. I will provide you a function that parse your Json data in Company object and i will left some comments in source code to try explain you the process. It's simple.
List<Company> _parseData(DataSnapshot dataSnapshot) {
List<Company> companyList =new List();
// here you replace List map = snapshot.value with... dataSnapshot.val()
Map<String, dynamic> mapOfMaps = Map.from( dataSnapshot.val() );
//actually dynamic here is another Map, String are the keys like -LXuQmyuF7E... or LXuSkMdJX...
//of your picture example and dynamic other Map
//in next lines we will interate in all values of your map
//remeber that the values are maps too, values has one map for each company.
//The values are Map<String, dynamic>
//in this case String are keys like category, company_name, country, etc...
//and dynamics data like string, int, float, etc
//parsing and adding each Company object to list
mapOfMaps.values.forEach( (value) {
companyList.add(
//here you'll not use fromSnapshot to parse data,
//i thing you got why we're not using fromSnapshot
Company.fromJson( Map.from(value) )
);
});
return companyList;
}
Note you will use Company.fromSnapshot(snapshot) when you read a specific company from
your db, something like ...
// just to illustrate
var snapshot = await FirebaseDatabase.instance
.reference()
.child("Companies")
.child(companyId).once();
Company.fromSnapshot( snapshot );
because in this case the snapshot.value is a single map.
Well another thing is, take a look at you initState method in your statefull widget, i don't know if is a good
approach call setState inside initState method even after a Future execution. This is just an advise.
snapshot.value returns a Map and you are using a List to store it. Maybe the easiest solution here is to use the var type to declare your map variable:
var map = snapshot.value;
but it's a good practice to set a type whenever you can to your variables. So, in this case the best option is to set it like this:
Map<dynamic, dynamic> map = snapshot.value;
Check dart's documentation to improve your skills in useing maps and list and how they work Lists docs Maps docs