Values retrieved from firestore only after hot reload in flutter - firebase

I am new to flutter. This is what I have done so far(This is not the whole code).As you can see I am trying get some data from firestore in flutter. The problem is that the number value is always zero when I entering to the page. But if I hot reload the page it gives me the correct value. I Why is that? I think it has something to do with future and async. But I don't understand them yet properly.
Widget build(BuildContext context) {
someMethod() {
return Firestore.instance
.collection('comments')
.where("Post", isEqualTo: widget.snapshot.documentID)
.getDocuments();
}
someMethod().then((QuerySnapshot snapshot) {
if (snapshot.documents.isNotEmpty) {
db = snapshot.documents;
}
});
int number = db?.length ?? 0;
print(number);
}

You have to use setState() to Notify the framework that the internal state of this object has changed.
someMethod().then((QuerySnapshot snapshot) {
if (snapshot.documents.isNotEmpty) {
setState(() {
db = snapshot.documents;
});
}
});

Related

Flutter subscribe/query to one field in one file in cloud firestore

New to booth flutter and stackoverflow.
I am making the account verification functionally for my flutter app. My plan is to divided this functionally into two parts, part one shows an alertdialog when the screen is built, and part two checks if the "activated" field in firestore is true or false. I have problem of making part two.
This is what I write for part one
String uid = "fdv89gu3njgnhJGBh";
bool isActivated = false;
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
if (isActivated == false) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: AlertDialog(
title: Text("Activation pending"),
content: Text("Your account is waiting to be activate by admin"),
actions: [
FlatButton(
child: Text("Refresh"),
onPressed: () {
// just bring reassurance to user
},
),
],
),
);
});
}
});
}
For part two I plan to make a Future return type function, what it will do is to subscribe the boolean value that stored in firestore: /user/uid/activated, once the function gets a "true" from firestore, it will return it to part one and part one will close the alertdialog(which I haven't figure out how to do this).
I've already seen some solutions from the internet but most solutions involve StreamBuilder, but it seems that I don't need to build any widget for the stream in part two. Is it better to just make changes to what I write previously* or integrate both parts two one StreamBuilder function?
*What I wrote for get the data from one field among all files (and this works well):
Future<bool> registeredCheck(String email) async {
var userInfo = await _firestore.collection("user").get();
for (var userInf in userInfo.docs) {
if (userInf.data()["email"] == email) {
return true;
}
}
return false;
}
}
Thank you
You don't have to query the entire collection. Since you already know the uid, you can just get the document of the uid directly like this:
Future<bool> registeredCheck(String email) async {
final userDoc = await _firestore.collection("user").doc(uid).get();
return userDoc.data()['activated'] ?? false;
}
The reason why I am adding ?? false is to return false instead of null when the activated value is null;

Admin access with flutter - hide and show widget and button based on User right firebase

I am working to build an admin access to a client. Among the visibility I need to constraint is the visibility of the button.
When changing access to user to admin, the button is not appearing back. The dependent boolean condition is mentioned below.
bool _show = false;
void showFloationButton() {
setState(() {
_show = true;
});
}
void hideFloationButton() {
setState(() {
_show = false;
});
}
void userAdminAccess() async {
FirebaseUser currentUser = await FirebaseAuth.instance.currentUser();
if ( currentUser != null) {
Firestore.instance
.collection('Users')
.where('isAdmin', isEqualTo: true);
} return showFloationButton();
}
Your code looks like it wants to perform a query, but it is not actually doing so. It's just building a Query object. You have to use get() to actually make that query happen, and await the response:
var querySnapshot = await Firestore.instance
.collection('Users')
.where('isAdmin', isEqualTo: true)
.get();
if (querySnapshot.size > 0) {
// there is at least one document returned by this query
}
else {
// there are not matching documents
}
I suggest learning more about how to perform queries in the documentation.
Note that what you're doing is potentially very expensive. It seems to me that you should probably get a single document for the user, identified by their UID, and look for a field within that document. Getting all admins could incur a lot of reads unnecessarily.

How to cache Firebase data in Flutter?

In my app I build a list of objects using data from Firebase. Inside a StreamBuilder, I check if the snapshot has data. If it doesen't, I am returning a simple Text widget with "Loading...". My problem is that if I go to another page in the app, and then come back, you can see for a split second that it says 'Loading...' in the middle of the screen, and it is a bit irritating. I am pretty sure it is downloading the data from Firebase, and building the widget every time I come back to that page. And if I don't do the check for data, it gives me a data that I am trying to access data from null.
Is there a way to cache the data that was already downloaded, and if there has been no change in the data from Firebase, then just use the cached data?
Heres a redacted version of my code:
class Schedule extends StatefulWidget implements AppPage {
final Color color = Colors.green;
#override
_ScheduleState createState() => _ScheduleState();
}
class _ScheduleState extends State<Schedule> {
List<Event> events;
List<Event> dayEvents;
int currentDay;
Widget itemBuilder(BuildContext context, int index) {
// Some Code
}
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
stream: Firestore.instance.collection('events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text("Loading...");
}
events = new List(snapshot.data.documents.length);
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
events[i] = Event(
name: doc["name"],
start: DateTime(
doc["startTime"].year,
doc["startTime"].month,
doc["startTime"].day,
doc["startTime"].hour,
doc["startTime"].minute,
),
end: DateTime(
doc["endTime"].year,
doc["endTime"].month,
doc["endTime"].day,
doc["endTime"].hour,
doc["endTime"].minute,
),
buildingDoc: doc["location"],
type: doc["type"],
);
}
events.sort((a, b) => a.start.compareTo(b.start));
dayEvents = events.where((Event e) {
return e.start.day == currentDay;
}).toList();
return ListView.builder(
itemBuilder: itemBuilder,
itemCount: dayEvents.length,
);
},
),
);
}
}
You can use the the following code to define the source you want to retrieve data from. This will search either in local cache or on the server, not both. It works for all get() parameters, no matter if it is a search or document retrieval.
import 'package:cloud_firestore/cloud_firestore.dart';
FirebaseFirestore.instance.collection("collection").doc("doc").get(GetOptions(source: Source.cache))
To check if the search has data in cache, you need to first run the search against cache and if there is no result, run it against the server.
I found project firestore_collection to use a neat extension that can greatly simplify this process.
import 'package:cloud_firestore/cloud_firestore.dart';
// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_document.dart
extension FirestoreDocumentExtension on DocumentReference {
Future<DocumentSnapshot> getSavy() async {
try {
DocumentSnapshot ds = await this.get(GetOptions(source: Source.cache));
if (ds == null) return this.get(GetOptions(source: Source.server));
return ds;
} catch (_) {
return this.get(GetOptions(source: Source.server));
}
}
}
// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_query.dart
extension FirestoreQueryExtension on Query {
Future<QuerySnapshot> getSavy() async {
try {
QuerySnapshot qs = await this.get(GetOptions(source: Source.cache));
if (qs.docs.isEmpty) return this.get(GetOptions(source: Source.server));
return qs;
} catch (_) {
return this.get(GetOptions(source: Source.server));
}
}
If you add this code, you can simply change the .get() command for both documents and queries to .getSavy() and it will automatically try the cache first and only contact the server if no data can be locally found.
FirebaseFirestore.instance.collection("collection").doc("doc").getSavy();
To be sure whether the data is coming from Firestore's local cache or from the network, you can do this:
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
print(doc.metadata.isFromCache ? "NOT FROM NETWORK" : "FROM NETWORK");
In the case you described you are probably going to still see the loading screen when its "NOT FROM NETWORK". This is because it does take some time to get it from the local cache. Soon you will be able to ask for the query's metadata for cases with empty results.
Like others suggested, you can cache the results and you won't see this. First you can try to cache it in the Widget using something like:
QuerySnapshot cache; //**
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
initialData: cache, //**
stream: Firestore.instance.collection('events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text("Loading...");
}
cache = snapshot.data; //**
This will make your widget remember the data. However, if this does not solve your problem, you would have to save it not in this widget but somewhere else. One option is to use the Provider widget to store it in a variable that lives beyond the scope of this particular widget.
Probably not related, but it's also a good idea to move the Firestore.instance.collection('events').snapshots() to initState(), save the reference to the stream in a private field and use that it StreamBuilder. Otherwise, at every build() you may be creating a new stream. You should be ready for build() calls that happen many times per second, whatever the reason.
Using Generics
Appending to #James Cameron's answer above; I found myself in a situation where, said implementation removed my typecast from withConverter. So, the below adds the generic types back into the functions.
main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
extension FirestoreDocumentExtension<T> on DocumentReference<T> {
Future<DocumentSnapshot<T>> getCacheFirst() async {
try {
var ds = await get(const GetOptions(source: Source.cache));
if (!ds.exists) return get(const GetOptions(source: Source.server));
return ds;
} catch (_) {
return get(const GetOptions(source: Source.server));
}
}
}
extension FirestoreQueryExtension<T> on Query<T> {
Future<QuerySnapshot<T>> getCacheFirst() async {
try {
var qs = await get(const GetOptions(source: Source.cache));
if (qs.docs.isEmpty) return get(const GetOptions(source: Source.server));
return qs;
} catch (_) {
return get(const GetOptions(source: Source.server));
}
}
}
use_case.dart
The implementation below would not compile with James example, as DocumentSnapshot<Object?> is not a subset of DocumentSnapshot<UserModel>. So, by adding the generic parameters back in, we can ensure that this extension maintains any type casts.
Future<DocumentSnapshot<UserModel>> userInfo() async {
return await FirebaseFirestore.instance
.doc("${path_to_user_model_doc}")
.withConverter<UserModel>(
fromFirestore: (snapshot, _) => UserModel.fromJson(snapshot.data()!),
toFirestore: (userModel, _) => userModel.toJson(),
)
.getCacheFirst();
}
pubspec.yaml
environment:
sdk: ">=2.17.1 <3.0.2"
dependencies:
cloud_firestore: ^3.1.17

Flutter/Firestore - Streambuilder and Firestore with realtime listener

I'm trying to set up a Firestore stream inside a stream builder like how they showed in their doc but when I update a doc or delete one it doesn't get reflected on the screen. The logs gets updated but not the actual screen.
Code:
// Chats API file
Stream<List<Message>> messagesStream({String chatID, String orderBy, bool descending, List<dynamic> startAfter, int limit}) {
return this
.collection
.document(chatID)
.collection(Config.messages)
.orderBy(orderBy, descending: descending)
.startAfter(startAfter)
.limit(limit)
.snapshots()
.map((event) => event.documents.map((documentSnapshot) => Message().model(id: documentSnapshot.documentID, map: documentSnapshot.data)).toList().reversed.toList());
}
// Chat file
Widget _streamBuilderWidget;
#override
void initState() {
super.initState();
this._streamBuilder();
if (Platform.isIOS) {
this._scrollController.addListener(() {
if (this._scrollController.position.pixels <= -135.0) {
this._checkForOldMessages();
}
});
}
}
#override
Widget build(BuildContext context) {
…
Expanded(child: this._streamBuilderWidget),
…
}
Widget _streamBuilder() {
return this._streamBuilderWidget = StreamBuilder<List<Message>>(
initialData: this._messages,
stream: APIs().chats.messagesStream(chatID: widget.chat.chatID, orderBy: 'createdAt', descending: true, startAfter: this._startAfter, limit: this._messagesLimit),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
{
return PAIndicator();
}
default:
{
if (snapshot.data.length < this._messagesLimit) {
this._hasMoreMessages = false;
} else {
this._hasMoreMessages = true;
}
snapshot.data.forEach((m) {
print(m.message); // Prints 10 values even when 1 file is changed
});
this._messages.insertAll(0, snapshot.data);
return Platform.isIOS
? MessagesList(mKey: this._listKey, scrollController: this._scrollController, messages: this._messages, aUser: widget.aUser)
: RefreshIndicator(
child: MessagesList(mKey: this._listKey, scrollController: this._scrollController, messages: this._messages, aUser: widget.aUser),
onRefresh: () async {
await Future.delayed(Duration(seconds: 1));
this._checkForOldMessages();
return null;
});
}
}
});
}
Also, when I send a message it doesn't get added to the list, I have to go back and enter the chatroom again to see the latest message.
How can I have it so that when I open the chat room I can see the new messages coming in while just showing the most recent messages to reduce costs, I don't want to show all 10000 messages. Just the most recent ones and the ones that are being added to it while users are in the chatroom.
Note: I have it like this because for somereason whenever I'm typing it keeps reloading and when I scroll the array just multiplies itself and what not. This is the only way I have add older messages to the array as I scroll that I know of.
The new messages do not show until I re-enter the chatroom.
With regards to how to only show the most recent messages, you should be implementing paginated queries and lazy load the older messages after a certain scroll point.
https://firebase.google.com/docs/firestore/query-data/query-cursors

Flutter Return Length of Documents from Firebase

Im trying to return the length of a list of documents with this function:
Future totalLikes(postID) async {
var respectsQuery = Firestore.instance
.collection('respects')
.where('postID', isEqualTo: postID);
respectsQuery.getDocuments().then((data) {
var totalEquals = data.documents.length;
return totalEquals;
});
}
I'm initialize this in the void init state (with another function call:
void initState() {
totalLikes(postID).then((result) {
setState(() {
_totalRespects = result;
});
});
}
However, when this runs, it initially returns a null value since it doesn't have time to to fully complete. I have tried to out an "await" before the Firestore call within the Future function but get the compile error of "Await only futures."
Can anyone help me understand how I can wait for this function to fully return a non-null value before setting the state of "_totalRespsects"?
Thanks!
I think you're looking for this:
Future totalLikes(postID) async {
var respectsQuery = Firestore.instance
.collection('respects')
.where('postID', isEqualTo: postID);
var querySnapshot = await respectsQuery.getDocuments();
var totalEquals = querySnapshot.documents.length;
return totalEquals;
}
Note that this loads all documents, just to determine the number of documents, which is incredibly wasteful (especially as you get more documents). Consider keeping a document where you maintain the count as a field, so that you only have to read a single document to get the count. See aggregation queries and distributed counters in the Firestore documentation.
Perfect code for your problem:
int? total;
getLength() async {
var getDocuments = await DatabaseHelper.registerUserCollection
.where("register", isEqualTo: "yes")
.get();
setState(() {
total = getDocuments.docs.length;
});
}
#override
void initState() {
super.initState();
getLength();
if (kDebugMode) {
print(total);
}
}

Resources