Flutter Firestore query 2 collections - firebase

Question is updated
How would I query 2 collections? I have a wishlist collection where each wishlist document looks like this:
documentID: "some-wishlist-id",
modified: "1564061309920",
productId: "2tqXaLUDy1IjxLafIu9O",
userId: "0uM7Dt286JYK6q8iLFyF4tG9cK53"
And a product collection where each product document looks like this. The productId in the wishlist collection would be a documentID in the product collection:
documentID: "2tqXaLUDy1IjxLafIu9O",
dateCreated: "1563820643577",
description: "This is a description for Product 9",
images: ["some_image.jpg"],
isNegotiable: true,
isSold: false,
rentPrice: 200,
sellPrice: 410,
title: "Product 9",
totalWishLists: 0,
userId: "0uM7Dt286JYK6q8iLFyF4tG9cK53"
NOTE: To be clear, the wishlist query should return a list of documents that I need to iterate over to retrieve the product.
Not sure if I need to use streams or futures in this case, but this is what I have so far:
Future<Product> getProduct(String documentId) {
return Firestore.instance
.collection(APIPath.products())
.document(documentId)
.get()
.then((DocumentSnapshot ds) => Product.fromMap(ds.data));
}
Query getWishListByUser(userId) {
return Firestore.instance
.collection(APIPath.wishlists())
.where('userId', isEqualTo: userId);
}
Future<List<Product>> getProductsWishList(userId) async {
List<Product> _products = [];
await getWishListByUser(userId)
.snapshots()
.forEach((QuerySnapshot snapshot) {
snapshot.documents.forEach((DocumentSnapshot snapshot) async {
Map<String, dynamic> map = Map.from(snapshot.data);
map.addAll({
'documentID': snapshot.documentID,
});
WishList wishList = WishList.fromMap(map);
await getProduct(wishList.productId).then((Product product) {
print(product.title); // This is printing
_products.add(product);
});
});
});
// This is not printing
print(_products);
return _products;
}
Thanks

With any database you'll often need to join data from multiple tables to build your view.
In relational databases, you can get the data from these multiple tables with a single statement, by using a JOIN clause.
But in Firebase (and many other NoSQL databases) there is no built-in way to join data from multiple locations. So you will have to do that in your code.
Create Wishlist Model:
class Wishlist {
Wishlist({
this.id,
this.modified,
this.productId,
this.userId
});
final String id;
final String modified;
final String productId;
final String userId;
Wishlist.fromMap(json)
: id = json['id'].toString(),
modified = json['modified'].toString(),
productId = json['productId'].toString(),
userId = json['userId'].toString();
}
And in your API file, do this:
final Firestore _fireStore = Firestore.instance;
getWishList(wishlistId) async {
return await _fireStore.collection('wishlists').document(wishlistId).get();
}
getProduct(productId) async {
return await _fireStore.collection('product').document(productId).get();
}
Future<List<Product>>getProductsWishList(wishlistId) async {
var _wishlists = null;
List<Product> _products = []; // I am assuming that you have product model like above
await getWishList(wishlistId).then((val) {
_wishlists = Wishlist.fromMap(val.data);
_wishlists.forEach((wish) {
await getProduct(wish.productId).then((product) {
_products.add(product));
});
});
});
return _products;
}

I found a solution last weekend but forgot to post it. Figured I do that now in case someone else has this problem.
I used a combination of StreamBuilder and FutureBuilder. Not sure if there is a better answer, perhaps combining multiple streams? But this worked for me.
return StreamBuilder<List<Wishlist>>(
stream: wishListStream(userId),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Wishlist> wishlists = snapshot.data;
if (wishlists.length > 0) {
return new GridView.builder(
scrollDirection: Axis.vertical,
itemCount: wishlists.length,
itemBuilder: (context, index) {
Future<Product> product =
getProduct(wishlists[index].productId);
return FutureBuilder(
future: product,
builder: (context, snapshot) {
if (snapshot.hasData) {
Product product = snapshot.data;
// Do something with product
} else {
return Container();
}
},
);
},
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
);
} else {
return Text('No products in your wishlist');
}
}
if (snapshot.hasError) {
print('WishlistPage - ${snapshot.error}');
return Center(child: Text('Some error occurred'));
}
return Center(child: CircularProgressIndicator());
},
);

Related

How can i get data from firebase by document ID in Flutter? [duplicate]

Edit: This Question is outdated, and I am sure, new documentation and more recent answers are available as of now.
I want to retrieve data of only a single document via its ID. My approach with example data of:
TESTID1 {
'name': 'example',
'data': 'sample data',
}
was something like this:
Firestore.instance.document('TESTID1').get() => then(function(document) {
print(document('name'));
}
but that does not seem to be correct syntax.
I was not able to find any detailed documentation on querying firestore within flutter (dart) since the firebase documentation only addresses Native WEB, iOS, Android etc. but not Flutter. The documentation of cloud_firestore is also way too short. There is only one example that shows how to query multiple documents into a stream which is not what i want to do.
Related issue on missing documentation:
https://github.com/flutter/flutter/issues/14324
It can't be that hard to get data from a single document.
UPDATE:
Firestore.instance.collection('COLLECTION').document('ID')
.get().then((DocumentSnapshot) =>
print(DocumentSnapshot.data['key'].toString());
);
is not executed.
but that does not seem to be correct syntax.
It is not the correct syntax because you are missing a collection() call. You cannot call document() directly on your Firestore.instance. To solve this, you should use something like this:
var document = await Firestore.instance.collection('COLLECTION_NAME').document('TESTID1');
document.get() => then(function(document) {
print(document("name"));
});
Or in more simpler way:
var document = await Firestore.instance.document('COLLECTION_NAME/TESTID1');
document.get() => then(function(document) {
print(document("name"));
});
If you want to get data in realtime, please use the following code:
Widget build(BuildContext context) {
return new StreamBuilder(
stream: Firestore.instance.collection('COLLECTION_NAME').document('TESTID1').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var userDocument = snapshot.data;
return new Text(userDocument["name"]);
}
);
}
It will help you set also the name to a text view.
Null safe code (Recommended)
You can either query the document in a function (for example on press of a button) or inside a widget (like a FutureBuilder).
In a method: (one time listen)
var collection = FirebaseFirestore.instance.collection('users');
var docSnapshot = await collection.doc('doc_id').get();
if (docSnapshot.exists) {
Map<String, dynamic>? data = docSnapshot.data();
var value = data?['some_field']; // <-- The value you want to retrieve.
// Call setState if needed.
}
In a FutureBuilder (one time listen)
FutureBuilder<DocumentSnapshot<Map<String, dynamic>>>(
future: collection.doc('doc_id').get(),
builder: (_, snapshot) {
if (snapshot.hasError) return Text ('Error = ${snapshot.error}');
if (snapshot.hasData) {
var data = snapshot.data!.data();
var value = data!['some_field']; // <-- Your value
return Text('Value = $value');
}
return Center(child: CircularProgressIndicator());
},
)
In a StreamBuilder: (always listening)
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: collection.doc('doc_id').snapshots(),
builder: (_, snapshot) {
if (snapshot.hasError) return Text('Error = ${snapshot.error}');
if (snapshot.hasData) {
var output = snapshot.data!.data();
var value = output!['some_field']; // <-- Your value
return Text('Value = $value');
}
return Center(child: CircularProgressIndicator());
},
)
If you want to use a where clause
await Firestore.instance.collection('collection_name').where(
FieldPath.documentId,
isEqualTo: "some_id"
).getDocuments().then((event) {
if (event.documents.isNotEmpty) {
Map<String, dynamic> documentData = event.documents.single.data; //if it is a single document
}
}).catchError((e) => print("error fetching data: $e"));
This is simple you can use a DOCUMENT SNAPSHOT
DocumentSnapshot variable = await Firestore.instance.collection('COLLECTION NAME').document('DOCUMENT ID').get();
You can access its data using variable.data['FEILD_NAME']
Update FirebaseFirestore 12/2021
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('YOUR COLLECTION NAME')
.doc(id) //ID OF DOCUMENT
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new CircularProgressIndicator();
}
var document = snapshot.data;
return new Text(document["name"]);
}
);
}
This is what worked for me in 2021
var userPhotos;
Future<void> getPhoto(id) async {
//query the user photo
await FirebaseFirestore.instance.collection("users").doc(id).snapshots().listen((event) {
setState(() {
userPhotos = event.get("photoUrl");
print(userPhotos);
});
});
}
Use this code when you just want to fetch a document from firestore collection , to perform some operations on it, and not to display it using some widget (updated jan 2022 )
fetchDoc() async {
// enter here the path , from where you want to fetch the doc
DocumentSnapshot pathData = await FirebaseFirestore.instance
.collection('ProfileData')
.doc(currentUser.uid)
.get();
if (pathData.exists) {
Map<String, dynamic>? fetchDoc = pathData.data() as Map<String, dynamic>?;
//Now use fetchDoc?['KEY_names'], to access the data from firestore, to perform operations , for eg
controllerName.text = fetchDoc?['userName']
// setState(() {}); // use only if needed
}
}
Simple way :
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('YOUR COLLECTION NAME')
.doc(id) //ID OF DOCUMENT
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new CircularProgressIndicator();
}
var document = snapshot.data;
return new Text(document["name"]);
}
);
}
var document = await FirebaseFirestore.instance.collection('Users').doc('CXvGTxT49NUoKi9gRt96ltvljz42').get();
Map<String,dynamic>? value = document.data();
print(value!['userId']);
You can get the Firestore document by following code:
future FirebaseDocument() async{
var variable = await FirebaseFirestore.instance.collection('Collection_name').doc('Document_Id').get();
print(variable['field_name']);
}
Use this simple code:
Firestore.instance.collection("users").document().setData({
"name":"Majeed Ahmed"
});

FutureBuilder's snapshot is always either null or has no data

I am fairly new to flutter and I hope this problem can be easily solved, but up until now I have no idea what's going wrong.
I have an app where I would like to return a list of users after the search button is pressed. I think that a FutureBuilder with a ListView.builder is the correct choice for this, but somehow I can't get my data to appear in the FutureBuilder even though my async function is definitely finding results.
My future:
Future<List<Map<dynamic, dynamic>>> results;
the button:
onPressed: () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
setState(() {
results = null;
searched = true;
});
results = getGuestData(widget.place['placeName'], query);
}
},
Async function:
Future<List<Map>> getGuestData(String placeName, Map query) async {
List<Map> result = [];
CollectionReference guestsRef = Firestore.instance
.collection(XXX)
.document(XXX)
.collection(XXX);
if (query['date'] == null) {
guestsRef
.where('lastName', isEqualTo: query['lastName'])
.where('firstName', isEqualTo: query['firstName'])
.getDocuments()
.then((doc) {
if (doc != null) {
result = doc.documents.map((x) => x.data).toList(); \\debugger shows this is working
}
}).catchError((e) => print("error fetching data: $e"));
}
return result;
and finally the problem:
FutureBuilder<List<Map>>(
future: results,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
List guests = snapshot.data;
return ListView.builder(
shrinkWrap: true,
itemCount: guests.length,
itemBuilder: (context, index) {
return ListTile(
... //do stuff here
);
},
);
} else {
return CircularProgressIndicator();
}
},
)
snapshot.data will indeed turn out to be a List, but the size is always 0 despite the getGuestData query finding users.

Flutter StreamBuilder for multiple firebase documents

I am trying to make my Flutter app update when a change is made to the usersCollection.document(user.uid) firebase document.
When the user document is updated I want to retrieve the data from this document but also from another firebase document, facilitiesCollection.document(...).
My current code
Future<Map> _getCheckedInFacilityData() async {
Map<String, dynamic> result = {};
try {
DocumentSnapshot userDoc =
await _db.usersCollection.document(user.uid).get();
if (userDoc.data['checkedIn']) {
// User is checked in
DocumentSnapshot facDoc = await _db.facilitiesCollection
.document(userDoc.data['activeFacilityID'].toString())
.get();
result['facilityID'] = userDoc.data['activeFacilityID'];
result['sessionID'] = userDoc.data['activeSessionID'];
result['facilityActiveUsers'] = facDoc.data['activeUsers'].length;
result['facilityName'] = facDoc.data['name'];
return result;
}
} catch (er) {
debugPrint(er.toString());
}
return null;
}
FutureBuilder<Map>(
future: _getCheckedInFacilityData(),
builder: (context, map) {
switch (map.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
...
This is currently working but the page is not updated when a change is made to the user document.
I haven't been using Flutter/Dart for long so any ideas are welcome.
Is it possible to return a custom object/map which is comprised of 2 separate documents from a StreamBuilder, or is there another method that will work in my situation.
Surely you can do it with Streams asyncMap() and then listen in StreamBuilder
Basic algoritm
Get stream of you first data type and then asyncMap to wait second data type and return them both
stream.asyncMap(
(v1) async {
final v2 = await Future.delayed(Duration(seconds: 1), () => 4);
return v1 * v2;
},
);
Closer to your code
Stream<Map<String, dynamic>> _getCheckedInFacilityData() {
return _db.usersCollection.document(user.uid).snapshots()
.asyncMap(
(userDoc) async {
final DocumentSnapshot facDoc =
await _db.facilitiesCollection
.document(userDoc.data['activeFacilityID'].toString())
.get();
final Map<String, dynamic> userMap = userDoc.data;
final Map<String, dynamic> facMap = facDoc.data;
return userMap..addAll(facMap);
},
);
}
In this function I merge two maps - be carefull if both maps have identical keys map will keep only last was added key in our case from addAll(facMap)
Last step is to show you streamed data on screen - use StreamBuilder
StreamBuilder<Map>(
stream: _getCheckedInFacilityData(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('${snapshot.error}');
} else if (snapshot.connectionState == ConnectionState.waiting) {
return LinearProgressIndicator();
}
return /* some widget that shows your data*/;
},
),

How to set the value of a model object in flutter and use it in FutureBuilder ListView?

I want to fetch data from the firestore and display it in the form of cards using ListView. I am willing the data fetched from firestore in an object for reusability on the same as well as other screens but, I am unable to do so. I created a model and a database service to serve it;
I tried to create a map which can store HomePage objects in it. Each object of the list creates a new card but, I am not able to assign values in the objects. I tried using the print statement below it but no output came there
Any leads on how to solve this problem would be really appreciated.
model
class HomePage {
bool confirmed;
DriverDetails driverDetail;
HomePage({this.confirmed, this.driverDetails});
}
class DriverDetails {
String driverUID;
String driverName;
String vehicalNumber;
String vehicalName;
String timeOfReporting;
String placeOfReporting;
LatLng reportingCord;
DriverDetails({
this.driverName,
this.driverUID,
this.placeOfReporting,
this.reportingCord,
this.timeOfReporting,
this.vehicalName,
this.vehicalNumber,
});
}
Database Service
class DatabaseService {
final Firestore _db = Firestore.instance;
static DriverDetails driverdetails;
static bool confirmed;
Map homeObject = new Map<String, HomePage>();
HomePage home = new HomePage(
confirmed: confirmed,
driverDetail: driverdetails,
);
/// Query a subcollection
Future streamHomePage(FirebaseUser user) async {
var ref = _db
.collection('homepage')
.document(user.uid)
.collection('h')
.document('28032020');
await ref.get(source: Source.serverAndCache).then((ref) => {
ref.data.forEach((index, value) => {
// Prints "index = WE10203 and value Swargate and null"
print(
"index = $index and value ${value['dd']['p']} and ${home.driverDetail}"),
driverdetails.placeOfReporting = value['dd']['p'],
// The below line prints nothing
print("driverdetails = $driverdetails"),
homeObject[index] = home
}),
});
return homeObject;
}
} }
Home Page screen
FutureBuilder(
future: databaseService(),
builder: (context, snapshot) {
if (snapshot.hasData) {
stringMap = snapshot.data;
}
return ListView.builder(
itemBuilder: (context, index) {
stringMap.forEach((index, value) => {
print("The stringMap is ${stringMap.keys.toList()}"),
});
return HomepageCards(
user: widget.user,
cardDetails: stringMap[stringMap.keys.toList()[index]],
);
},
itemCount: stringMap.length,
scrollDirection: Axis.vertical,
controller: _controller,
shrinkWrap: true,
);
},
)
databaseService() async {
return DatabaseService().streamHomePage(widget.user);
}
Try using models like this :
import 'package:cloud_firestore/cloud_firestore.dart';
class EmployeeData {
final DocumentReference reference;
String address;
String designation;
String email;
EmployeeData.data(
this.reference, [
this.address,
this.designation,
this.email,
]);
factory EmployeeData.from(DocumentSnapshot document) => EmployeeData.data(
document.reference,
document.data['Address'],
document.data['Designation'],
document.data['Email'],
);
void save() {
reference.setData(toMap());
}
Map<String, dynamic> toMap() {
return {
'address': address,
'designation': designation,
'email': email,
};
}
}
so when u got data u can use like this :
return StreamBuilder<QuerySnapshot>(
stream: Firestore().collection('Workers').snapshots(),
builder: (context, snapshot) {
if (snapshot.data != null) {
// Here u will get list of document snapshots
final List<DocumentSnapshot> documents = snapshot.data.documents;
final List<EmployeeData> employeeDataList = documents
.map((snapshot) => EmployeeData.from(snapshot))
.toList();
// now u can access each document by simply specifying its number
// u can also use list view to display every one of them
return ListView.builder(
itemCount: documents.length,
itemBuilder: (context, int index) => Text(employeeDataList[index].email),
);
} else {
// Show loading indicator here
}
},
);

Query a single document from Firestore in Flutter (cloud_firestore Plugin)

Edit: This Question is outdated, and I am sure, new documentation and more recent answers are available as of now.
I want to retrieve data of only a single document via its ID. My approach with example data of:
TESTID1 {
'name': 'example',
'data': 'sample data',
}
was something like this:
Firestore.instance.document('TESTID1').get() => then(function(document) {
print(document('name'));
}
but that does not seem to be correct syntax.
I was not able to find any detailed documentation on querying firestore within flutter (dart) since the firebase documentation only addresses Native WEB, iOS, Android etc. but not Flutter. The documentation of cloud_firestore is also way too short. There is only one example that shows how to query multiple documents into a stream which is not what i want to do.
Related issue on missing documentation:
https://github.com/flutter/flutter/issues/14324
It can't be that hard to get data from a single document.
UPDATE:
Firestore.instance.collection('COLLECTION').document('ID')
.get().then((DocumentSnapshot) =>
print(DocumentSnapshot.data['key'].toString());
);
is not executed.
but that does not seem to be correct syntax.
It is not the correct syntax because you are missing a collection() call. You cannot call document() directly on your Firestore.instance. To solve this, you should use something like this:
var document = await Firestore.instance.collection('COLLECTION_NAME').document('TESTID1');
document.get() => then(function(document) {
print(document("name"));
});
Or in more simpler way:
var document = await Firestore.instance.document('COLLECTION_NAME/TESTID1');
document.get() => then(function(document) {
print(document("name"));
});
If you want to get data in realtime, please use the following code:
Widget build(BuildContext context) {
return new StreamBuilder(
stream: Firestore.instance.collection('COLLECTION_NAME').document('TESTID1').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var userDocument = snapshot.data;
return new Text(userDocument["name"]);
}
);
}
It will help you set also the name to a text view.
Null safe code (Recommended)
You can either query the document in a function (for example on press of a button) or inside a widget (like a FutureBuilder).
In a method: (one time listen)
var collection = FirebaseFirestore.instance.collection('users');
var docSnapshot = await collection.doc('doc_id').get();
if (docSnapshot.exists) {
Map<String, dynamic>? data = docSnapshot.data();
var value = data?['some_field']; // <-- The value you want to retrieve.
// Call setState if needed.
}
In a FutureBuilder (one time listen)
FutureBuilder<DocumentSnapshot<Map<String, dynamic>>>(
future: collection.doc('doc_id').get(),
builder: (_, snapshot) {
if (snapshot.hasError) return Text ('Error = ${snapshot.error}');
if (snapshot.hasData) {
var data = snapshot.data!.data();
var value = data!['some_field']; // <-- Your value
return Text('Value = $value');
}
return Center(child: CircularProgressIndicator());
},
)
In a StreamBuilder: (always listening)
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: collection.doc('doc_id').snapshots(),
builder: (_, snapshot) {
if (snapshot.hasError) return Text('Error = ${snapshot.error}');
if (snapshot.hasData) {
var output = snapshot.data!.data();
var value = output!['some_field']; // <-- Your value
return Text('Value = $value');
}
return Center(child: CircularProgressIndicator());
},
)
If you want to use a where clause
await Firestore.instance.collection('collection_name').where(
FieldPath.documentId,
isEqualTo: "some_id"
).getDocuments().then((event) {
if (event.documents.isNotEmpty) {
Map<String, dynamic> documentData = event.documents.single.data; //if it is a single document
}
}).catchError((e) => print("error fetching data: $e"));
This is simple you can use a DOCUMENT SNAPSHOT
DocumentSnapshot variable = await Firestore.instance.collection('COLLECTION NAME').document('DOCUMENT ID').get();
You can access its data using variable.data['FEILD_NAME']
Update FirebaseFirestore 12/2021
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('YOUR COLLECTION NAME')
.doc(id) //ID OF DOCUMENT
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new CircularProgressIndicator();
}
var document = snapshot.data;
return new Text(document["name"]);
}
);
}
This is what worked for me in 2021
var userPhotos;
Future<void> getPhoto(id) async {
//query the user photo
await FirebaseFirestore.instance.collection("users").doc(id).snapshots().listen((event) {
setState(() {
userPhotos = event.get("photoUrl");
print(userPhotos);
});
});
}
Use this code when you just want to fetch a document from firestore collection , to perform some operations on it, and not to display it using some widget (updated jan 2022 )
fetchDoc() async {
// enter here the path , from where you want to fetch the doc
DocumentSnapshot pathData = await FirebaseFirestore.instance
.collection('ProfileData')
.doc(currentUser.uid)
.get();
if (pathData.exists) {
Map<String, dynamic>? fetchDoc = pathData.data() as Map<String, dynamic>?;
//Now use fetchDoc?['KEY_names'], to access the data from firestore, to perform operations , for eg
controllerName.text = fetchDoc?['userName']
// setState(() {}); // use only if needed
}
}
Simple way :
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('YOUR COLLECTION NAME')
.doc(id) //ID OF DOCUMENT
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new CircularProgressIndicator();
}
var document = snapshot.data;
return new Text(document["name"]);
}
);
}
var document = await FirebaseFirestore.instance.collection('Users').doc('CXvGTxT49NUoKi9gRt96ltvljz42').get();
Map<String,dynamic>? value = document.data();
print(value!['userId']);
You can get the Firestore document by following code:
future FirebaseDocument() async{
var variable = await FirebaseFirestore.instance.collection('Collection_name').doc('Document_Id').get();
print(variable['field_name']);
}
Use this simple code:
Firestore.instance.collection("users").document().setData({
"name":"Majeed Ahmed"
});

Resources