How can i get data from a subcollection in firebase in flutter? - firebase

i m building an app using flutter and firebase as backend .
i m storring my data in a collection named 'Users' . every user has data and has a subcollection named 'Transactions' which are the transactions that he made .
i want to get these informations
Here is my DataBase
enter image description here
enter image description here
class DatabaseService {
final String uid;
DatabaseService({this.uid});
final CollectionReference usersCollection =
FirebaseFirestore.instance.collection('Users');
List<Transaction1> TransactionsListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map((doc) {
return Transaction1(
uid: doc.data()['uid'],
name: doc.data()['name'],
description: doc.data()['description'],
time: doc.data()['time'],
argent: doc.data()['argent'],
somme: doc.data()['somme'],
deleted: doc.data()['deleted']);
}).toList();
}
Stream<List<Transaction1>> get transactions{
return
usersCollection.doc(uid).collection('Transactions').snapshots().map(TransactionsListFromSnapshot);
}
}
and this is the flutter code
body: StreamBuilder<List<Transaction1>>(
stream: DatabaseService(uid: this.widget.uid).transactions,
builder: (context, snapshot) {
return snapshot.hasData ? Stack(
children: [
ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
print('hhhhh');
return Transaction_Card(data: snapshot.data[index]);
},
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
child: Icon(
Icons.add,
color: Colors.white,
),
onPressed: () {
_showMyDialog();
},
))
],
) : Stack(
children: [
Text('Error'),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
child: Icon(
Icons.add,
color: Colors.white,
),
onPressed: () {
_showMyDialog();
},
))
],
);
}),
Can anyone help me please

I just did this yesterday. In my parent document I add an array in which I store the document id of the sub collection documents.
Then, in the repository I have the following function (projects are the main documents, areas are the sub collection documents
#override
Stream<List<Area>> areas(String projectId) {
final CollectionReference areaCollection =
FirebaseFirestore.instance.collection('projects').doc(projectId).collection('areas');
return areaCollection.snapshots().map((snapshot) {
return snapshot.docs
.map((doc) => Area.fromEntity(AreaEntity.fromSnapshot(doc)))
.toList();
});
}
Then, I have used the example of the bloc documentation with entities and models to transform the data.
For each (sub) collection I have a separate listener, so one for projects, and one for areas of my currently focussed project. If new areas arrive, I check whether I already know them (by id) or if I have to add a new area to the project.
for (var a in event.areas) {
if (!areaPointer.containsKey(a.areaId)) { // add new area to list
areaPointer[a.areaId] = project.areas.length;
project.areas.add(a);
}
else // update existing area
project.areas[areaPointer[a.areaId]] = a;
}
As you see, I also have an areaPointer which stores the relationship of area.id and index of project.areas[i]

Related

How to get all sub collection data to Widget with StreamBuilder in Firebase + Firestore + Flutter

In flutter, I use Firebase Firestore to save data. I created the root collection as Users and in that collection, each user has a subcollection named by Books.
This is my Firebase Cloud Firestore
I tried to get all users all books with a button click as below
getUserBooks() {
FirebaseFirestore.instance.collection("Users").get().then((querySnapshot) {
querySnapshot.docs.forEach((result) {
FirebaseFirestore.instance
.collection("Users")
.doc(result.id)
.collection("Books")
.get()
.then((querySnapshot) {
querySnapshot.docs.forEach((result) {
BooksModel booksModel = BooksModel.fromDocument(result);
print("--------------------- Books ---------------------\n"
"id: ${booksModel.bookID}\n"
"name: ${booksModel.bookName}\n"
"image: ${booksModel.bookImage}");
});
});
});
});
}
This is model class
class BooksModel {
final String bookID;
final String bookName;
final String bookImage;
BooksModel({
this.bookID,
this.bookName,
this.bookImage,
});
factory BooksModel.fromDocument(DocumentSnapshot documentSnapshot) {
return BooksModel(
bookID: documentSnapshot['bookID'],
bookName: documentSnapshot['bookName'],
bookImage: documentSnapshot['bookImage'],
);
}
}
I used the below code to get the book details according to a single user but I want to get all users books details to that widget. How can I change the code?
_usersBookListWidget() {
return Container(
height: MediaQuery.of(context).size.height,
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection("Users")
.doc(userID)
.collection('Books')
.snapshots(),
builder: (context, snapshot) {
return !snapshot.hasData
? Container()
: snapshot.data.docs.length.toString() == "0"
? Container(
height: 250.0,
width: 200.0,
child: Column(
children: [
SizedBox(
height: 30.0,
),
Text(
"You have no books yet",
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.headline6
.fontSize),
),
Image.asset(
'assets/icons/icon_no_books_yet.png',
height: 100.0,
width: 100.0,
),
],
),
)
: ListView.builder(
scrollDirection: Axis.vertical,
physics: BouncingScrollPhysics(),
itemCount: snapshot.data.docs.length,
itemBuilder: (BuildContext context, int index) {
BooksModel booksModel =
BooksModel.fromDocument(snapshot.data.docs[index]);
return Column(
children: [
Text(booksModel.bookID),
Text(booksModel.bookName),
],
);
},
);
},
),
);
}
There is not a direct way yet to get all sub-collections for all the documents in a single collection like in your case.
First you have to query your users collection, then after that, for each user document, you run a separate query. Here's a pseudo code:
1- StreamBuilder (FirebaseFirestore.instance.collection.('Users').snapshots(),
Here you can display info about every user in a listview if you want, but you have to capture the document.id, because you will need it for the next step.
2- you create a widget called userBooks for example, for this widget, you pass to it the document.id from the previous step. Now you have all the user IDs in your database, and since the subcollection is called books and doesn't change, you use this for another streambuilder, with this collection reference:
FirebaseFirestore.instance.collection.('Users').doc(document.id).collection('Books').snapshots(). This will give you the result you want.
For your code snippet to work, you need to use async\await, like this:
getUserBooks() async {
await FirebaseFirestore.instance.collection("Users").get().then((querySnapshot) async {
querySnapshot.docs.forEach((result) {
await FirebaseFirestore.instance
.collection("Users")
.doc(result.id)
.collection("Books")
.get()
.then((querySnapshot) {
querySnapshot.docs.forEach((result) {
BooksModel booksModel = BooksModel.fromDocument(result);
print("--------------------- Books ---------------------\n"
"id: ${booksModel.bookID}\n"
"name: ${booksModel.bookName}\n"
"image: ${booksModel.bookImage}");
});
});
});
});
}
This will work for getting them in a single button.

Flutter App Performance Issues with Long List

I have a flutter app where user can add items to a list which are stored in firebase. User can add up to 1000 items at once. Initially this is no issue but with a growing number of list items the app gets slower and slower until when adding multiple items at once after roughly 1000 items are in the list it crashes the app due to the memory use -
thread #10, name = 'io.flutter.1.ui', stop reason = EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=1450 MB, unused=0x0)
How can I improve the code so the performance improves. I would like to keep the setup with the Stream since it lets me dynamically filter the list on the fly. One information here as well is that WidgetA and WidgetB also both use the Stream Data to display the number of list items in the list.
Here is my code a bit simplified for ease of reading:
Main Screen Class:
Widget content(context) {
double h = MediaQuery.of(context).size.height; //screen height
double w = MediaQuery.of(context).size.width; //screen width
return StreamProvider<List<Activity>>.value(
catchError: (_, __) => null,
value: DatabaseService().activities(widget.uid),
builder: (context, snapshot) {
return SafeArea(
child: Container(
//color: Theme.of(context).backgroundColor, //SkyHookTheme.background,
child: Scaffold(
backgroundColor: Colors.transparent,
body: NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: Stack(children: [
ListView(
controller: _scrollController,
children: <Widget>[
Column(
children: <Widget>[
WidgetA(),
WidgetB(),
ActivityList(), //List of User Activities
],
)
],
),
]),
),
),
),
);
});
}
ActivityList Class Listview Building:
ListView buildList(List<Activity> acts){
items = ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
itemCount: len,
itemBuilder: (context, index) {
return ActivityTile(activity: acts[index], number: acts.length - (index));
},
);
return items;
}
Any Tips / Hints how I can improve this would be highly appreciated.
Thanks!
You have to pagination to achieve smooth perform
And just load 10 documents in one time and with
Help of scrollcontroller check you are end of the list
And then load next 10 documents that’s would be
Efficient manner .
Instead of "listview" use sliversList widget.
See the Example of sliversList and sliverscomponents here
I think #AmitSingh's suggestion is best but if you want to load data in once then you can get data in pagination but not when the user scrolls but when you got the first bunch of data.
yeah you should use pagination or lazy-loading! reading and rendering 1000 document at once is too much work for most mobile devices.
instead you should load you documents likes this
import 'package:cloud_firestore/cloud_firestore.dart';
Firestore firestore = Firestore.instance
class LongList extends StatefulWidget {
#override
_LongListState createState() => _LongListState();
}
class _LongListState extends State<LongList> {
List<DocumentSnapshot> products = []; // stores fetched products
bool isLoading = false; // track if products fetching
bool hasMore = true; // flag for more products available or not
int documentLimit = 10; // documents to be fetched per request
DocumentSnapshot lastDocument; // flag for last document from where next 10 records to be fetched
ScrollController _scrollController = ScrollController(); // listener for listview scrolling
getProducts() async {
if (!hasMore) {
print('No More Products');
return;
}
if (isLoading) {
return;
}
setState(() {
isLoading = true;
});
QuerySnapshot querySnapshot;
if (lastDocument == null) {
querySnapshot = await firestore
.collection('products')
.orderBy('name')
.limit(documentLimit)
.getDocuments();
} else {
querySnapshot = await firestore
.collection('products')
.orderBy('name')
.startAfterDocument(lastDocument)
.limit(documentLimit)
.getDocuments();
print(1);
}
if (querySnapshot.documents.length < documentLimit) {
hasMore = false;
}
lastDocument = querySnapshot.documents[querySnapshot.documents.length - 1];
products.addAll(querySnapshot.documents);
setState(() {
isLoading = false;
});
}
void initState(){
getProducts();
_scrollController.addListener(() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.20;
if (maxScroll - currentScroll <= delta) {
getProducts();
}
});
_pageManager = PageManager();
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Pagination with Firestore'),
),
body: Column(children: [
Expanded(
child: products.length == 0
? Center(
child: Text('No Data...'),
)
: ListView.builder(
controller: _scrollController,
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
contentPadding: EdgeInsets.all(5),
title: Text(products[index]['name']),
subtitle: Text(products[index] ['short_desc']),
);
},
),
),
isLoading
? Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(5),
color: Colors.yellowAccent,
child: Text(
'Loading',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
)
: Container()
]),
);
}
}

Can't get actual String download url from Firebase Storage and only returns Instance of 'Future<String>' even using async/await

I am trying to get user avatar from firebase storage, however, my current code only returns Instance of 'Future<String>' even I am using async/await as below. How is it possible to get actual download URL as String, rather Instance of Future so I can access the data from CachedNewtworkImage?
this is the function that calls getAvatarDownloadUrl with current passed firebase user instance.
myViewModel
FutureOr<String> getAvatarUrl(User user) async {
var snapshot = await _ref
.read(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
if (snapshot != null) {
print("avatar url: $snapshot");
}
return snapshot;
}
getAvatarURL is basically first calling firebase firestore reference then try to access to the downloadURL, if there is no user data, simply returns null.
Future<String> getAvatarDownloadUrl(String code) async {
Reference _ref =
storage.ref().child("users").child(code).child("asset.jpeg");
try {
String url = await _ref.getDownloadURL();
return url;
} on FirebaseException catch (e) {
print(e.code);
return null;
}
}
I am calling these function from HookWidget called ShowAvatar.
To show current user avatar, I use useProvider and useFuture to actually use the data from the database, and this code works with no problem.
However, once I want to get downloardURL from list of users (inside of ListView using index),
class ShowAvatar extends HookWidget {
// some constructors...
#override
Widget build(BuildContext context) {
// get firebase user instance
final user = useProvider(accountProvider.state).user;
// get user avatar data as Future<String>
final userLogo = useProvider(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
// get actual user data as String
final snapshot = useFuture(userLogo);
// to access above functions inside of ListView
final viewModel = useProvider(myViewModel);
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: snapshot.data, // **this avatar works!!!** so useProvider & useFuture is working
),
),
SizedBox(height: 32),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Center(
child: Column(
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: viewModel
.getAvatarUrl(goldWinners[index].user)
.toString(), // ** this avatar data is not String but Instance of Future<String>
),
),
),
],
),
);
},
itemCount: goldWinners.length,
),
Avatar() is simple statelesswidget which returns ClipRRect if avatarURL is not existed (null), it returns simplace placeholder otherwise returns user avatar that we just get from firebase storage.
However, since users from ListView's avatarUrl is Instance of Future<String> I can't correctly show user avatar.
I tried to convert the instance to String multiple times by adding .toString(), but it didn't work.
class Avatar extends StatelessWidget {
final String avatarUrl;
final double radius;
final BoxFit fit;
Avatar({Key key, this.avatarUrl, this.radius = 16, this.fit})
: super(key: key);
#override
Widget build(BuildContext context) {
print('this is avatar url : ' + avatarUrl.toString());
return avatarUrl == null
? ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Image.asset(
"assets/images/avatar_placeholder.png",
fit: fit,
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: CachedNetworkImage(
imageUrl: avatarUrl.toString(),
placeholder: (_, url) => Skeleton(radius: radius),
errorWidget: (_, url, error) => Icon(Icons.error),
fit: fit,
));
}
}
Since the download URL is asynchronously determined, it is returned as Future<String> from your getAvatarUrl method. To display a value from a Future, use a FutureBuilder widget like this:
child: FutureBuilder<String>(
future: viewModel.getAvatarUrl(goldWinners[index].user),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return snapshot.hashData
? Avatar(avatarUrl: snapshot.data)
: Text("Loading URL...")
}
)
Frank actually you gave an good start but there are some improvements we can do to handle the errors properly,
new FutureBuilder(
future: //future you need to pass,
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, i) {
DocumentSnapshot ds = snapshot.data.docs[i];
return //the data you need to return using /*ds.data()['field value of doc']*/
});
} else if (snapshot.hasError) {
// Handle the error and stop rendering
GToast(
message:
'Error while fetching data : ${snapshot.error}',
type: true)
.toast();
return new Center(
child: new CircularProgressIndicator(),
);
} else {
// Wait for the data to fecth
return new Center(
child: new CircularProgressIndicator(),
);
}
}),
Now if you are using a text widget as a return statement in case of errors it will be rendered forever. Incase of Progress Indicators, you will exactly know if it is an error it will show the progress indicator and then stop the widget rendering.
else if (snapshot.hasError) {
}
else {
}
above statement renders until, if there is an error or the builder finished fetching the results and ready to show the result widget.

Flutter asyncMap not run until setState

I am making a chat app that displays both a Group Chat and Private Chat in the same List.
I use Firestore as the database and store the data of User, Group and Contact in there. I have a Message Screen that displays a list of Chats that the User has using StreamBuilder.
I want to display data differently depending on the group's data. The group chat has their Group picture, Private Chat with User in Contact, their avatar display, and Private Chat with a generic icon display with User not in Contact.
I iterate through the stream first in a DatabaseService class, then put it in a variable and set it as a stream for StreamBuilder. This works fine, but I also want a list to check if a user already has a private chat with another User without getting the data from Firestore.
API.dart
//this is where I put my code to connect and read/write data from Firestore
final FirebaseFirestore _db = FirebaseFirestore.instance;
Api();
....
Stream<QuerySnapshot> streamCollectionByArrayAny(
String path, String field, dynamic condition) {
return _db
.collection(path)
.where(field, arrayContainsAny: condition)
.snapshots();
}
DatabaseService.dart
...
List<GroupModel> groups; //List of Groups
Stream<List<GroupModel>> groupStream; //Stream of List Group
...
Stream<QuerySnapshot> fetchGroupsByMemberArrayAsStream(
String field, dynamic condition) {
return _api.streamCollectionByArrayAny('groups', field, condition);
}
//function to get Contact Detail using List of Group User
Future<ContactModel> getContactDetail(List<dynamic> members) async {
//remove current user id from the list
members.removeWhere((element) => element.userId == user.userId);
//getContactbyId return a ContactModel object from Firestore
ContactModel contactModel =
await getContactById(user.userId, members.first.userId);
if (contactModel != null && contactModel.userId.isNotEmpty) {
return contactModel;
} else {
return new ContactModel(
userId: members.first.userId, nickname: "", photoUrl: "");
}
}
Future<GroupModel> generateGroupMessage(GroupModel group) async {
//check if Group Chat or Private chat
if (group.type == 1) {
ContactModel contactModel = await getContactDetail(group.membersList);
group.groupName = contactModel.nickname.isNotEmpty
? contactModel.nickname
: contactModel.userId;
group.groupPhoto = contactModel.photoUrl;
}
print("Add");
//add the group data into List<GroupModel> groups
groups.add(group);
return group;
}
void refreshMessageList() {
groups = [];
print("refresh");
//get Group Data as Stream from FireStore base on the user data in the Member Array of Group then map it to Stream while also change data base on Group type in generateGroupMessage
groupStream = fetchGroupsByMemberArrayAsStream('membersList', [
{"isActive": true, "role": 1, "userId": user.userId},
{"isActive": true, "role": 2, "userId": user.userId}
]).asyncMap((docs) => Future.wait([
for (GroupModel group in docs.docs
.map((doc) => GroupModel.fromMap(doc.data()))
.toList())
generateGroupMessage(group)
]));
}
Message.dart
#override
void initState() {
super.initState();
...
databaseService.refreshMessageList();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.symmetric(horizontal: 16),
margin: EdgeInsets.only(top: 24),
child: Column(
children: [
...
Flexible(
child: StreamBuilder(
stream: databaseService.groupStream,
builder: (context, AsyncSnapshot<List<GroupModel>> snapshot) {
if (!snapshot.hasData) {
print("No data");
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),
),
);
} else {
print("Has data");
groups = List.from(snapshot.data);
groups.removeWhere(
(element) => element.recentMessageContent.isEmpty);
groups.sort((group1, group2) {
if (DateTime.parse(group1.recentMessageTime)
.isAfter(DateTime.parse(group2.recentMessageTime))) {
return -1;
} else {
return 1;
}
});
return ListView.builder(
padding: EdgeInsets.all(10.0),
itemBuilder: (context, index) =>
buildItem(context, groups[index]),
itemCount: groups.length,
),
),
),
}
],)));
}
Widget buildItem(BuildContext context, GroupModel group) {
if (group.recentMessageContent == '') {
return Container();
} else {
return Column(
children: [
Container(
child: InkWell(
child: Row(
children: <Widget>[
Material(
child: group.groupPhoto.isNotEmpty
? CachedNetworkImage(
placeholder: (context, url) => Container(
child: CircularProgressIndicator(
strokeWidth: 1.0,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.grey),
),
width: 60.0,
height: 60.0,
padding: EdgeInsets.all(10.0),
),
imageUrl: group.groupPhoto,
width: 60.0,
height: 60.0,
fit: BoxFit.cover,
)
: Icon(
group.type == 1
? Icons.account_circle
: Icons.group,
size: 60.0,
color: Colors.grey,
),
borderRadius: BorderRadius.all(Radius.circular(30.0)),
clipBehavior: Clip.hardEdge,
),
SizedBox(
width: 150,
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
group.groupName,
style: TextStyle(
color: colorBlack,
fontSize: 12,
fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
Text(
group.recentMessageContent,
style: TextStyle(
color: Colors.grey,
fontSize: 10,
height: 1.6),
overflow: TextOverflow.ellipsis,
),
],
),
margin: EdgeInsets.only(left: 12.0),
),
),
Spacer(),
Text(
formatDateTime(group.recentMessageTime),
style: TextStyle(color: Colors.grey, fontSize: 10),
),
],
),
onTap: () {
switch (group.type) {
case 1:
Navigator.of(context, rootNavigator: true)
.push(MaterialPageRoute(
settings:
RouteSettings(name: "/message/chatPage"),
builder: (context) => ChatPage(group: group)))
.then((value) => setState);
break;
case 2:
Navigator.of(context, rootNavigator: true)
.push(MaterialPageRoute(
settings:
RouteSettings(name: "/message/chatGroup"),
builder: (context) =>
ChatGroupPage(group: group)))
.then((value) => {setState(() {})});
break;
}
}),
),
Divider(
color: Colors.grey,
),
],
);
}
}
The ChatPage and ChatGroupPage navigate to Private Chat and Group Chat respectively, and in there the User can add the chat partner or group member into Contact.
When adding is done I call the databaseService.refreshMessageList to refresh the Stream of List Group, so when I navigate back to the Message Screen, it will refresh and display accordingly. However, the List<GroupModel> groups becomes blank and will not add data until I navigate back to the Message Screen.
I debugged the app and found that the List became blank because it executes groups = [] but did not run the .asyncMap until I hot reload or navigate Message Screen and put the setState in .then to refresh the data.
I need the List groups to check whether the 2 users already have a private chat to create a new one when adding to Contact. I have already tried putting setState after databaseService.refreshMessageList, but it still did not work.
Can anyone please help me and provide a solution? I know this is not a good question to ask, but I have been stuck with this for almost a week now and desperately need an answer. Thank you in advance.
EDIT
Here is my data structure:
Users
/users (collection)
/userId
/user (document)
- userId
- nickname
- photoUrl
- token
- /contacts (subcollection)
/contactId
/contact (document)
- userId
- nickname
- photoUrl
Groups:
/groups (collection)
/groupId
/group (document)
- groupId
- groupName
- type
- membersList (List<Map<String, dynamic>>)
- member: userId, isActive, role
- recentMessageContent
- recentMessageTime
- recentMessageType
Messages:
/messages (collection)
/groupId
/groupMessage (document)
/messages (subcollection)
/messageId
/message (document)
- messageContent
- messageTime
- messageType
You can use array membership, for example, the array-contains method can query for elements within an array without performing any manipulation. There is an interesting article that provides some examples you might interest you.
Another alternative could be to iterate both arrays until matching the values you need. However, iteration can lead to performance issues if you do not implement it correctly.

Retrieving documents data into a List where documentID is the same as currentUser().uid - Flutter

I had a function in my CustomerNotifier class that reads all customers as a list from Firebase as below:
getCustomers(CustomerNotifier customerNotifier) async {
String userId = (await FirebaseAuth.instance.currentUser()).uid;
print('Current logged in user uid is: $userId');
var snapshot = await customerCollection
.orderBy('created_at', descending: true)
.getDocuments();
List<Customer> _customerList = [];
snapshot.documents.forEach((document) {
Customer customer = Customer.fromMap(document.data);
_customerList.add(customer);
});
customerNotifier.customerList = _customerList;
}
I have another function to updates or creates a new customer and saves to Firebase as below:
Future updateCustomer(Customer customer, bool isUpdating) async {
CollectionReference customerRef =
await Firestore.instance.collection('customer');
if (isUpdating) {
customer.updatedAt = Timestamp.now();
await customerRef.document().updateData(customer.toMap());
print('updated customer with id: ${customer.id}');
} else {
customer.createdAt = Timestamp.now();
DocumentReference documentReference =
await customerRef.add(customer.toMap());
customer.id = documentReference.documentID;
print('created customer successfully with id: ${customer.id}');
await documentReference.setData(customer.toMap(), merge: true);
addCustomer(customer);
}
notifyListeners();
}
With both methods above, I used to successfully read and write customer data to my Firebase. However, I am trying to only read data created and updated by the currently signed in User. So suggestions from other stackoverflow threads, I've been advised to set my customer.id to userId, where userId == currentUser().uid. I can successfully write to my DB using an updated version of my updateCustomer as below:
Future updateCustomer(Customer customer, bool isUpdating) async {
CollectionReference customerRef =
await Firestore.instance.collection('customer');
FirebaseUser user = await FirebaseAuth.instance.currentUser();
String userId = user.uid;
print('Current logged in user uid is: $userId');
if (isUpdating) {
customer.updatedAt = Timestamp.now();
await customerRef.document(userId).updateData(customer.toMap());
print('updated customer with id: ${customer.id}');
} else {
customer.createdAt = Timestamp.now();
DocumentReference documentReference = await customerRef.document(userId);
// add(customer.toMap());
customer.id = documentReference.documentID;
print('created customer successfully with id: ${customer.id}');
await documentReference.setData(customer.toMap(), merge: true);
addCustomer(customer);
}
notifyListeners();
}
How do I proceed to read customer data from firebase only created by currentUser() since documentID/customer.id is now equals to userId fo the currentUser() logged in?
Here's what I've tried so far:
getCustomers(CustomerNotifier customerNotifier) async {
String userId = (await FirebaseAuth.instance.currentUser()).uid;
print('Current logged in user uid is: $userId');
QuerySnapshot snapshot = await Firestore.instance
.collection('customers')
.where('id', isEqualTo: userId)
.orderBy('created_at', descending: true)
.getDocuments();
List<Customer> _customerList = [];
snapshot.documents.forEach((document) {
Customer customer = Customer.fromMap(document.data);
_customerList.add(customer);
});
customerNotifier.customerList = _customerList;
}
//customer_screen.dart
//this uses a ListView.builder to display all customers created by currentUser()
class CustomersScreen extends StatefulWidget {
static String id = 'customers';
#override
_CustomersScreenState createState() => _CustomersScreenState();
}
class _CustomersScreenState extends State<CustomersScreen> {
bool showSpinner = true;
bool _isInit = true;
#override
void initState() {
if (_isInit) {
showSpinner = true;
} else {
showSpinner = false;
}
CustomerNotifier customerNotifier =
Provider.of<CustomerNotifier>(context, listen: false);
customerNotifier.getCustomers(customerNotifier);
super.initState();
}
#override
Widget build(BuildContext context) {
CustomerNotifier customerNotifier = Provider.of<CustomerNotifier>(context);
Future<void> _resfreshList() async {
customerNotifier.getCustomers(customerNotifier);
}
return Scaffold(
drawer: DrawerClass(),
appBar: AppBar(
title: Text(
'All customers',
style: kAppBarTextStyle,
),
backgroundColor: kAppBarColour,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
customerNotifier.currentCustomer = null;
Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) {
return CustomerFormScreen(isUpdating: false);
}));
},
child: Icon(Icons.add),
backgroundColor: kThemeIconColour,
),
// body: showSpinner
// ? Center(child: CircularProgressIndicator())
body: RefreshIndicator(
child: Consumer<CustomerNotifier>(
builder: (context, customer, child) {
return customer == null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
PaddingClass(bodyImage: 'images/empty.png'),
SizedBox(
height: 20.0,
),
Text(
'You don\'t have any customer',
style: kLabelTextStyle,
),
],
)
: Padding(
padding: const EdgeInsets.only(top: 50.0),
child: ListView.separated(
itemBuilder: (context, int index) {
return Card(
margin: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 0.0),
elevation: 15.0,
color: Colors.white70,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
height: 100.0,
child: Icon(
FontAwesomeIcons.userCircle,
color: kThemeIconColour,
size: 50.0,
),
),
SizedBox(width: 20.0),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(' ${customer.customerList[index].firstName}' +
' ${customer.customerList[index].lastName}'),
SizedBox(
height: 8.0,
),
Text(
' ${customer.customerList[index].phoneNumber}'),
SizedBox(
height: 8.0,
),
Text(
' ${customer.customerList[index].email}'),
],
),
GestureDetector(
onTap: () {
customerNotifier.currentCustomer =
customerNotifier.customerList[index];
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) {
return CustomerDetailsScreen();
}));
},
child: Icon(
FontAwesomeIcons.caretDown,
color: kThemeIconColour,
),
),
],
),
);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(
height: 20.0,
);
},
itemCount: customerNotifier.customerList.length,
),
);
},
),
onRefresh: _resfreshList,
),
);
}
}
Thanks.
EDIT2:
Check out the compound queries here: https://firebase.google.com/docs/firestore/query-data/queries
Specifically this section:
db.collection("cities").where("capital", "==", true)
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
You could use this structure. 'cities' is 'customer' for you, and "capital" is the field on your document that you've saved as userIdWhenYouCreatedThisCustomerDocument, and instead of true you'd put your current user id. Of course, this uses .then() and you could do that or assign the result to a variable using await.
I'll note that you should review the documentation as you work through this, particularly as it regards doing it this way vs. doing a subcollection by user ID. Either are correct, but if you go beyond a proof of concept you'll find the phrase "where clauses are not filters" in firestore to be a reason to consider the latter approach. Of course, that may not matter at all in the end.
EDIT:
based on new information
Why are you using QuerySnapshot? You are (currently) retrieving a document snapshot for this, so you can use that directly.
Alternatively, can you post a screenshot of your firestore, with senstive data removed? I'm wondering if you are intending to store your data in the user document, like the immediate retrieval query code expects as-written and your upsert code delivers, as opposed to storing it in a collection within your user document.
If the latter, the code from the other answer would probably work better for you and your current code since it is set up to read from a collection, not a document. Of course, either approach could work just as well. The problem is that right now your upsert and the first half of your query is doing the former and halfway through your retrieval query you switch to the latter.
I think you can combine the answer to your other question (Read data from Firebase created by loggedin user - Flutter) with the documentation to find your solution here.
This uses a more up to date version of firestore than you're using, but provides a good starting point.
https://firebase.google.com/docs/firestore/query-data/get-data
For example.
DocumentReference ref =
Firestore.instance.collection('YourCollectionNameInFireStore').document(user.uid);
var temp = await docRef.getDocument();
// do something with the data if you want.. not sure of syntax for old version on
// this, for new version it is .data()
temp.data.<field>

Resources