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?
Related
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).
I Have a CollectionGroup query which displays all document data from 'school_news' collection in a Listtile with Streambuilder and ListView.
But now I want to only show the document data in a ListView/Listtile where 'name' field equals name in a list , like :
List userSelections = ['Liebenberg', 'St Michaels' , 'School C' ];
FirebaseFirestore.instance
.collectionGroup('school_news')
.where('name', arrayContainsAny: userSelections )
.snapshots(),
So what I want now is to only then display the document data fields in a ListView/Listtile where the 'name' fields are Liebenberg, St Michaels and 'School C' instead of all the document data in 'school_news' collection .
Is something like that possible ? I tried with the code above but with no luck, So I am not sure what I'm missing .
Any help or guidance would be greatly appreciated .Thank you
UPDATE
I have updated as per answer and included full code , as below :
class UserHome extends StatefulWidget {
const UserHome({Key? key}) : super(key: key);
#override
_UserHomeState createState() => _UserHomeState();
}
class _UserHomeState extends State<UserHome> {
final uid = FirebaseAuth.instance.currentUser!.uid;
final db = FirebaseFirestore.instance;
List userSelections = ['Liebenberg', 'St Michaels'];
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collectionGroup('school_news')
.where('name', whereIn: userSelections)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: Text('Loading '),
);
} else
return ListView(
children: snapshot.data!.docs.map((doc) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {},
child: Column(
children: [
ListTile(
leading: Text(doc['title']),
),
SizedBox(
height: 10,
),
],
),
),
);
}).toList(),
);
},
),
);
}
}
Seems like no data is returned as the page just displays 'Loading' .
The link for creating the new index for the 'WhereIn' I also dont get .
Anything I am doin wrong ?
Thanks for the help so far .
You should use whereIn instead of arrayContainsAny.
replace this line
.where('name', arrayContainsAny: userSelections )
with this one:
.where('name', whereIn: userSelections)
Remember that Firestore has a built-in constraint where you can't have more than 10 items in your list. It means your list userSelections cannot have more than 10 values (otherwise you may want to do the filter client side, depending on your use case)
Also, in your 1st execution, you may need to add a new index... just follow the web link (in the new error message) that will help create this required index directly in Firestore.
I am having a trouble reading collection from firebase and saving values in a list.
I basically have a collection called 'brands' where I have car brands like this:
Firebase 'brands' collection screenshot
I need these car brands to be saved as a list like this, to be able to use it in a dropdown menu as items:
<String>[
'ferrari',
'mercedes',
'porsche',
]
I have tried using StreamBuilder (below) but it requires to return a widget and I do not actually need a widget to be returned, so below StreamBuilder is just an experiment "in progress".
Do you have any ideas?
final stream = FirebaseFirestore.instance
.collection('accounts')
.doc('dealers')
.collection(user!.uid)
.doc(dealerName)
.collection('brands')
.snapshots();
StreamBuilder(
stream: stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text('Error in receiving snapshot: ${snapshot.error}');
}
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor,
),
);
}
return ListView.builder(
padding: EdgeInsets.all(8),
reverse: true,
itemCount: snapshot.data.docs!.length,
itemBuilder: (BuildContext context, int index) {
return Text(
snapshot.data.docs[index]['brandName'],
);
},
);
},
);
Once you get the data from firebase, loop through it and add the car brands to your list. Try this:
List<String> myBrands = [];
final dataRef = await FirebaseFirestore.instance
.collection('accounts')
.doc('dealers')
.collection(user!.uid)
.doc(dealerName)
.collection('brands')
.get();
dataRef.docs.forEach((doc) {
myBrands.add(doc.data()['brandName']);
});
You should then be able to use the myBrands list for your dropdown menu.
I am fetching a list using streambuilder from firebase and then compare with another list(Streambuilder) from firebase in flutter. how to compare?
eg: 1 person has 1000 followers and when 2nd person vists 1st person followers it has to show whether the followers(1st person) are also in his(2nd person) list or not.
below image is where i am fetching followers data of another person from firebase. also i have to check whether the followers of another person is in my following list or not.
class FollowersWid extends StatelessWidget {
final String userid;
FollowersWid(this.userid);
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('users/$userid/followers')
.snapshots(),
builder: (context, fetchedData) {
if (fetchedData.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
elevation: 8,
child: ListView.builder(
itemCount: fetchedData.data.docs.length,
itemBuilder: (ctx, index) => Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
UnFollow(fetchedData.data.docs[index]['username'], userid)
],
),
));
}
},
);
}
}
For this you can use .where and .contains eg.
List firebaseList1 = [0, 5, 1];
List firebaseList2 = [1, 0, 2];
print(firebaseList1.where((item) => firebaseList2.contains(item)));
//output: (0,1)
This happens in my main application
and
I replicate it with the given codelabs:
https://codelabs.developers.google.com/codelabs/flutter-firebase/index.html?index=..%2F..index#10
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Baby Names',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Baby Name Votes')),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('baby').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot){
return ListView(
padding: const EdgeInsets.only(top: 20.0),
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
return Padding(
key: ValueKey(record.name),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5.0),
),
child: ListTile(
title: Text(record.name),
trailing: Text(record.votes.toString()),
onTap: () => print(record),
),
),
);
}
}
class Record {
final String name;
final int votes;
final DocumentReference reference;
Record.fromMap(Map<String, dynamic> map, {this.reference})
: assert(map['name'] != null),
assert(map['votes'] != null),
name = map['name'],
votes = map['votes'];
Record.fromSnapshot(DocumentSnapshot snapshot)
: this.fromMap(snapshot.data, reference: snapshot.reference);
#override
String toString() => "Record<$name:$votes>";
}
Only plugin i use is the cloud_firestore 0.9.5+2.You need to be patient with this testing process please. You will not see the issue right away. Run the app first, set this project up. You can follow the directions in the given codelabs. Once everything is set up on front end and backend(create documents on firstore). Go have a lunch break, dinner, play video games or hang out with friends. Come back after 1 hour. Run the app, you will be incurred charges for those reads as new. Do it again, come back 1 hour and it will happen again
How to replicate it in real life:
Start this app by given code from codelabs. Run it, it should incur 4 document reads if you stored 4 documents into the firestore.
Start it up again. No reads are charged. Great it works! but no, it really doesnt.
I wake up next day and open up the app, im charged 4 reads. Okay maybe some magic happened. I restart it right away and no charges incur(great!). Later in 1 hour, i start up the app and I get charged 4 reads to display the very same 4 documents that have not been changed at all.
The problem is, on app start up. It seems to be downloading documents from the query snapshot. No changes have been made to the documents. This stream-builder has been previously run many times.
Offline mode(airplane mode), the cached data is displayed with no issues.
in my main application for example, I have a photoUrl and on fresh App start, you can see it being loaded from the firestore(meaning downloaded as a fresh document thus incurring a READ charge). I restart my main application, no charges are made and photo does not refresh(great!). 1 hour later i start up the app and charges are made for every document I retrieve(none of changed).
Is this how cloud firestore is supposed to behave?
From what I have read, its not supposed to behave like this :(
You should not do actual work in build() except building the widget tree
Instead of code like this in build
stream: Firestore.instance.collection('baby').snapshots(),
you should use
Stream<Snapshot> babyStream;
#override
void initState() {
super.initState();
babyStream = Firestore.instance.collection('baby').snapshots();
}
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: babyStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
The FutureBuilder docs don't mention it that explicitly but its the same
https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateConfig, or
State.didChangeDependencies. It must not be created during the
State.build or StatelessWidget.build method call when constructing the
FutureBuilder. If the future is created at the same time as the
FutureBuilder, then every time the FutureBuilder's parent is rebuilt,
the asynchronous task will be restarted.