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).
Related
I'm making this realy simple project where when you click a button, it increments a value in the realtime databse. This works, but I want a Text widget to update everytime the value changes. I've really new to this so please excuse my lack of knowledge. I have noticed that there's no snapshots methods available that would let me do something like this, so how I update a label to be the data in the realtime storage eveyime it changes? Here's the code which I use to increment the values-
Future<void> addData(int id) async {
DatabaseReference pointsRef = FirebaseDatabase.instance.ref("$id");
DataSnapshot snapshot = await pointsRef.get();
pointsRef.set((snapshot.value as int) + 1);
}
Use StreanBuilder like so:
StreamBuilder(
stream: FirebaseDatabase.instance.ref("id").onValue,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return Text(snapshot.data.toString());
},
)
In my App I use a Streambuilder to show a list of plans that are stored in Firestore (every plan has its own document) on a screen:
final CollectionReference planCol = FirebaseFirestore.instance.collection('users').doc(FirebaseAuth.instance.currentUser.uid).collection('plans');
body: StreamBuilder(
stream: planCol.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final planDocs = snapshot.data.docs;
return ListView.builder(
itemCount: planDocs.length,
itemBuilder: (context, index) => MyListTile(
title: Text(planDocs[index]['name']),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () {
edit(planDocs[index]);
},
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ExerciseTable(
plan: UserPlan(
name: planDocs[index]['name'],
exerciseNames: planDocs[index]
['exerciseNames'],
rows: planDocs[index]['rows'],
planId: planDocs[index].id,
),
);
},
),
);
}),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}));
On this screen, I only want to show the name of each document and not all the other data which is stored in the document(because they are very big). Now I was asking myself how efficient Streambuilder works when each document is very big. Does it load the whole document or only the field of the document, that is needed?
On this screen I only want to show the name of each document and not all the other data which is stored in the document(because they are very big).
All Cloud Firestore listeners fire on the document level. So there is no way you can get only the value of a single field in a document. That's the way Firestore works.
Now I was asking myself how efficient Streambuilder works when each document is very big.
It's more about the time that it takes to download those documents.
Does it load the whole document or only the field of the document, that is needed?
It loads the entire document.
If you need only some names, you should consider storing those names in a single document. In this case, you can read only one document and you have to pay only one read.
I want to create a revision/examination app for 20k+ students. I am going to take a test every day for all students (users app) of my app. and then store their records(scores) as total marks on the cloud Firestore.
so my problem is I want to display students' rank order by their scores(while sorting their total marks).
so if a student goes to the rank page to see the top students, the app will call 20k documents ordered by their total marks descending,
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection('Users')
.doc('Students')
.collection('Regions')
.doc('regionName')
.collection('Districts')
.doc('DIstrictName')
.collection('Student_Phone')
.orderBy('Total_Marks', descending: true)
//time registered students
.orderBy('Created', descending: false) the right one
.snapshots(),
builder: (BuildContext context, snapshot) {
return ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, index) {
return showUsers(
index: index,
snapshot: stuSnapShot.data.docs[index],
);
});
.......}
as well as I have to show 'my score page' for the student to show him his rank this will also want to retrieve the 20k documents to iterate through using this.
class showUsers extends StatefulWidget {
var snapshot;
final int index;
showUsers({#required this.snapshot, #required this.index});
#override
_showUsersState createState() => _showUsersState();
}
class _showUsersState extends State<showUsers> {
#override
Widget build(BuildContext context) {
var numNum = widget.snapshot.data()['PhoneNumber'].toString();
if (numNum == '+8977106429') {
return Container(
child: Text(
'you are :${widget.index} phone =$numNum',
style: TextStyle(
color: Colors.red, fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
}
}
. and this will cost a lot of money to the firebase cloud Firestore pricing when 20k students read 20k documents per day 20,0000 X 20,000= 400,0000,0000 ==400m reads per day, which will cost= $720 per day which equals 30 X $720=$21,600.
so is there another way to read these documents without that cost?
I'm creating a chat screen. What I'm currently doing is that I'm using a Streambuilder to listen to the 'messages' collection and display the messages using ListView.builder().
Below is the code i'm using.
StreamBuilder<QuerySnapshot>(
stream: _fireStoreInstance
.collection('$collectionName/$docID/messages')
.orderBy('sentAt', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return Center(
child: CircularProgressIndicator(),
);
List<Map> documents = snapshot.data.docs
.map((doc) => {'documentId': doc.id, ...doc.data()})
.toList();
return ListView.builder(
cacheExtent: MediaQuery.of(context).size.height,
reverse: true,
itemCount: documents.length,
padding:
const EdgeInsets.only(left: 15.0, right: 15.0, bottom: 5.0),
itemBuilder: (context, index) {
return MessageBubble(
...
);
},
);
},
),
My concern is, will the query fetch all the documents in the collection all at once? If yes then it will be a lot of reads each time the query is executed
_fireStoreInstance
.collection('$collectionName/$docID/messages')
.orderBy('sentAt', descending: true)
.snapshots();
Do I need to paginate by using limit ? If I paginate how do I listen to new messages ? Thank you for your help.
Yes, .snapshots() will read and keep listening to all documents that fit the query, if you want a subset of that you will have to paginate it using .limit().
I have found this article, with a video step by step on How to perform real-time pagination with Firestore with the use of an infinite scroll. I think this is exactly what you looking for, so I won't post any code since you can follow that example.
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!