Google Firestore how to query multiple sub-collections in Flutter - firebase

I am using Firestore in my project - for each user I have document and in each document I have subcollections (recent, planned).
items
document (uid)
sub-collection (recent)
doc1 (several fields)
doc2 ...
sub-collection (planned)
doc1 ...
doc2 ...
Currently, I am using two StreamBuilders in my app - one for getRecent getter and second for getPlanned, but I think, that this is not a good approach.
Is out there any better way, how to query these two sub collections (in the future, there will be more..) in one stream? (Or how to optimize my program to not use much bandwidth)
Here is example of my code in Flutter
// Get planned (getter in Data service class)
Stream<dynamic> get getPlanned {
return data
.document(uid)
.collection("planned")
.snapshots()
.map(_snapshotToPlanned);
}
// Stream Builder in Home widget
StreamBuilder(
stream: Data(uid: user.uid).getPlanned,
builder: (context, snapshot) {
if (snapshot.hasData) {
plannedTrips = snapshot.data;
if (snapshot.data.length == 0) return NothingWidget();
return PageView.builder(
pageSnapping: true,
controller: _pageController,
scrollDirection: Axis.horizontal,
itemCount: plannedTrips.length,
itemBuilder: (context, i) {
return PlannedTrip(planned: plannedTrips[i]);
},
);
} else ...
Do you have any idea, how to deal with this problem?
Thanks!

Related

Fetching data from Firestore for a leaderboard screen not efficient

I want to show a list of top 25 players (fetched from Firestore) on a screen and currently this is how I implemented it:
late final Stream<QuerySnapshot> _mainScoreStream;
#override
void initState() {
futureAd = fetchAd();
_mainScoreStream = FirebaseFirestore.instance
.collection('users')
.orderBy('current_score', descending: true)
.where('current_score', isGreaterThan: 0)
.limit(25)
.snapshots();
super.initState();
}
#override
Widget build(BuildContext context) {
// Used to make text size automaticaly sizeable for all devices
final double unitHeightValue = MediaQuery.of(context).size.height * 0.01;
final user = Provider.of<UserModels?>(context);
return SafeArea(
child: StreamBuilder<QuerySnapshot>(
stream: _mainScoreStream,
// ignore: missing_return
builder: (context, snapshot) {
if (snapshot.hasData) {
return Expanded(
child: ListView.builder(
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
DocumentSnapshot data = snapshot.data!.docs[index];
return LeaderboardCard(
currentScore: data['current_score'].toString(),
name: data['name'],
index: index,
isCurrentUser: data.id == user!.uid,
);
},
itemCount: snapshot.data!.docs.length,
),
);
} else if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
);
}
return Container();
},
),
);
}
}
The leaderboard changes once a day, however with this implementation I get an extra 25 reads per user.
Is there a more efficient way to fetch this data or is this okay since my daily reads per one user are around 30 ?
EDIT: I am aware that over optimizing my reads/writes is not a good practice, but currently based on Firebase Pricing Calculator this could lead to a lot of daily reads, so not sure how to go about it, I could always decrease the limit or remove the Leaderboard completely
If you change the leaderboard once per day, you could:
Calculate the leaderboard contents on a trusted system (your development machine, a server that you control, or Cloud Functions/Cloud Run), and store that in a separate document in Firestore. Then each client only has to read that document to get the entire leaderboard.
Create a data bundle with the query results each day on a trusted system, and distribute that to your users through a cheaper system (e.g. Cloud Storage or even as a document in Firestore again).

Flutter Firebase Cloud Firestore get stream of list of documents by their ids

Hey guys I have two top level collections, a user and a tabs collection. Within each user document I have an array field of document ids with each element in the array representing a document in the tabs collection (sort of like a reference or a pointer to that document) and I would like to listen to real time changes in the users/document with the list of ID's and to listen for changes to the corresponding documents in the tabs collection.
Below is my code so far and there are two issues with it. The stream isn’t working client side - I’m not receiving an updated snapshot when I update the document in cloud firestore. Also in order for me to get a list of tabIds I am currently making a query to cloud firestore which returns a stream however, the whereIn operator only accepts a list of objects so I’m not to sure how to handle that.
data model screenshot
Stream<List<Tab>> get tabs {
var tabIds = _db.collection('users').doc(user.uid).snapshots().where((event) => event.get('tabs'));
return _db
.collection('tabs')
.where(FieldPath.documentId, whereIn: tabIds)
.snapshots()
.map((list) =>
list.docs.map((doc) => Tab.fromJson(doc.data())).toList());
}
You can't directly listen to documents based on their ids, but you can add a field called docId (the value should be the id of the document) to each document then listen to collection with this where condition.
List listOfDocIds = []; // this is the list of your docIds
StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance
.collection('users')
.where('docId', whereIn: listOfDocIds),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
if (snapshot.hasError) return Text('Something went wrong');
if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
List<Map<String, dynamic>> data =
snapshot.data.docs.map((e) => e.data()).toList();
print(data);
// you can build your widget here
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, i) {
return ListTile(
title: Text(data[i]['name']),
// TODO: change 'name' to name of field in users document
);
},
);
},
),

Flutter FirebaseFirestore where condition returning related and unrelated values

I am querying a firestore collection in Flutter using where and arrayContains, for some reason it is not working as expected for me.
StreamBuilder(
stream: (_searchTerm.length >= 3)
? FirebaseFirestore.instance.collection("users").snapshots()
: FirebaseFirestore.instance
.collection('users')
.where('email', arrayContains: _searchTerm)
.snapshots(),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
final results = snapshot.data.docs;
print(results.length);
return ListView.builder(
shrinkWrap: true,
itemCount: results.length,
itemBuilder: (ctx, index) => Text(
results[index].data()['display-name'],
),
);
})
The _searchTerm variable is populated as I type in some values into the textfield and when it hits a length of three characters that's when the above query fires.
For example when I type in test the query should only return the values that contain test in it, but I am getting the whole collection with and without the value test.
Please advice!
EDIT - Posting a screenshot of my firestore data structure
When you do the following:
FirebaseFirestore.instance
.collection('users')
.where('email', arrayContains: _searchTerm)
.snapshots(),
You are looking for documents inside the users collection that have _searchTerm as an item of the email array, property of a user document.
There are two problems:
I don't think the email property of your users is an array.
Firebase does not perform substring searches
I think you will need to use a third-party application for searches on Firestore. A popular one is Algolia that comes with a quite powerful FREE plan.

Get only documents in a FireBase collection that have an id matching with documents in a separate collection

I have the following database structure:
users
|_____user01
|_____groups
|_____0002
|_____0004
|_____0006
groups
|_____0001
|_____0002
|_____0003
|_____0004
|_____0005
|_____0006
I currently have a StreamBuilder() that creates a list with all the entries it finds in root/groups, so it creates a list of 6 elements.
I need it to create a list using the entries in root/groups as it's doing now, but only those that have an id that matches the IDs of entries present in root/users/user01/groups, so it should create a list with 3 elements (0002, 0004 and 0006).
I can't wrap my head around how to achieve that.
This is my StreamBuild() :
StreamBuilder(
stream: FirebaseFirestore.instance.collection("groups").snapshots(),
builder: (context, snapshots) {
if (snapshots.hasData) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshots.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot documentSnapshot =
snapshots.data.documents[index];
return Dismissable(
child: Card(
)
)
Should I make a firebase query using where() that excludes documents IDs not shared by the two collections and feed the results to the StreamBuilder?
I would recommend you to use a FutureBuilder for every document inside the list you are creating using StreamBuilder and rendering using ListView.builder.
Create a future that matches each and every document in the list with documents inside the sub-collection of user i.e, groups.
Future<bool> matchDocs(String id)async{
final result = await FirebaseFirestore.instance.collection('users').doc(FirebaseAuth.instance.currentUser.uid).collection('groups').doc(id).get();
return result.exists;
}
Inside itemBuilder function return a FutureBuilder.
StreamBuilder(
stream:
FirebaseFirestore.instance.collection("groups").snapshots(),
builder: (context, snapshots) {
if (snapshots.hasData) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshots.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot documentSnapshot =
snapshots.data.documents[index];
return FutureBuilder<bool>(
future: matchDocs(
snapshots.data.documents[index].data()['id'].toString()),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data) {
return Dismissible(key: null, child: null);
}
}
return Container();
}
);
}
);
}
}
)
Note: I did not have any idea about your exact data models, so you
should be careful while implementing this solution.

flutter: Retrieving multiple document id present inside a collection of firebase

I want to get the id of every document in a collection. i tried this code but it return a single document id unlimited times. Can you please suggest a correction or a alternative
getrequest() {
linkref.document(uuid).
collection("Requests").getDocuments().then((value) async{
value.documents.forEach((doc) {
user.add(doc.documentID);
});},);
return ListView.builder(
itemBuilder: (BuildContext cntxt, int index){
return Text(user[index]);
});
}
there is a collection inside a document and inside this collection i have other document. i want to retrieve all documents id
This is screenshot of my firestore
Currently, you have just one document in firestore. I would suggest you add multiple documents and then test this command.You can use snapshot to get multiple documents as suggested by Sandeep using
Firestore.instance
.collection('Links')
.document(docID)
.collection('Requests')
.snapshots();
You can retrieve multiple documents with one request by querying documents in a collection. By default, Cloud Firestore retrieves all documents that satisfy the query in ascending order by document ID, but you can order and limit the data returned. To retrieve the documents conditionally using where() and then use get
More can be found in the documentation here
To use ListView builder, I would suggest using StreamBuilder something like this:
StreamBuilder<DocumentSnapshot>(
stream: Firestore()
.collection('homepage')
.document(widget.user.uid)
.collection('h')
.document(todaysDate())
.snapshots(),
builder: (context, snapshot) {
if (snapshot.data != null) {
snapshot.data.data.forEach((index, individualDetail) {
cardDetails[index] = individualDetail;
});
}
return cardDetails == null
? CircularProgressIndicator()
: ListView.builder(
itemBuilder: (context, index) {
return HomepageCards(
user: widget.user,
cardDetails:
cardDetails[cardDetails.keys.toList()[index]],
);
},
itemCount: (cardDetailKeys == null
? 0
: cardDetailKeys.length),
);
},
)
This is a snippet from my code but the StreamBuilder would look similar for you too.
Firestore.instance
.collection('Links')
.document(docID)
.collection('Requests')
.snapshots();

Resources