Initialising variable in initState - firebase

I'm initialising a variable in initState(){}:
#override
void initState() {
getDataFromFirestore();
super.initState();
});
}
The method is asyn and basically gets data from Firestore to populate an object '_markerMap' with data. This property is then used as a property in a widget. And this widget is called in my build method.
Widget build(BuildContext context) {
return new Scaffold(
body: MyWidget(
markerMap: _markerMap)
);
....
}
MyWidget is a calendar. markerMaps adds icons to certain dates on the calendar. The markers are only sometimes added to the calendar. So the failure is intermittent. Is is safe to assume that in initState() the data will load from firestore to initialise the variable from Firestore. Any thoughts what might be happening and why only sometimes the markers show up on the calendar?
adding code where _markerMap is set
getDataFromFirestore() async {
await FirebaseAuth.instance.currentUser().then((user) {
Firestore.instance.collection('availableDates').where('bandId', isEqualTo: user.uid).snapshots().listen(
(data) => data.documents.forEach((doc) => _markerMap.add(
doc['availableDates'].toDate(),
Event(
date:doc['availableDates'].toDate(),
title: 'hello',
icon: _presentIcon(doc['availableDates'].toDate().day.toString())))));
setState(() {});
}).catchError((onError){
});
}

As I can see from your getDataFromFirestore method, you perform the widget rebuild (setState call) right after you get the User object (FirebaseAuth.instance.currentUser() call).
However, you modify _markerMap variable later on - only when your Firestore.instance.collection query is complete. In this case setState call where it is right now is redundant.
Calling setState inside of your listen callback should solve the problem.
e.g.
final _markerMap = {};
getDataFromFirestore() async {
await FirebaseAuth.instance.currentUser().then((user) {
Firestore.instance
.collection('availableDates')
.where('bandId', isEqualTo: user.uid)
.snapshots()
.listen((data) => data.documents.forEach((doc) {
setState(() { // Here is the setState call
_markerMap.add(
doc['availableDates'].toDate(),
Event(
date: doc['availableDates'].toDate(),
title: 'hello',
icon: _presentIcon(doc['availableDates'].toDate().day.toString())
)
);
});
}));
}).catchError((onError) {});
}
Please double check this example code. My general advice is correct, however I did not test this on my end.

There's nothing wrong with initializing variables in initState unless they are static. So whats happening is you have declared _markerMap initially but it will only get initialed after some async function getDataFromFirestore(), which could take few seconds. And here you have assigned markerMap: _markerMap but initially _markerMap is null and only get initialed after getDataFromFirestore() function. Therefore its a good practice to either check for null
_markerMap!=null?MyWidget(
markerMap: _markerMap):Container();
or provide a default value

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;

Flutter FutureBuilder throws error, but on simulation it works as expected

I'm a beginner to Flutter and currently I'm having a problem with correctly implementing FutureBuilder in flutter.
I'm trying to build a user page, where I have my user information stored in Firebase, and every time I access the user page it retrieves the current user data and shows the data on the page. Here's the code I've written for the implementation:
class UserPage extends StatefulWidget{
#override
UserPageState createState() => UserPageState();
}
class UserPageState extends State<UserPage>{
String userName;
String userEmail;
String collegeName;
Future _infoInit() async {
userName = await HelperFunctions.getUserNamePreference();
userEmail = await HelperFunctions.getUserEmailPreference();
collegeName = await HelperFunctions.getUserCollegePreference();
}
Widget userScaffold(BuildContext context, AsyncSnapshot snapshot){
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(
userName
),
backgroundColor: Colors.lightBlue,
),
body:Center(
child: Text("This is User Page")
)
);
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _infoInit(),
builder: (context,AsyncSnapshot snapshot) => userScaffold(context, snapshot)
);
}
}
At the moment the only part I've written is showing the currently logged in user on the app bar, and when I run the code, it seems that it's successfully running. However, when I look at the android studio console I could see that it's actually facing and error which I think is related to async function executed in the FutureBuilder widget.
Error Message:
Performing hot reload...
Syncing files to device iPhone 11...
Reloaded 7 of 650 libraries in 397ms.
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building FutureBuilder<dynamic>(dirty, state: _FutureBuilderState<dynamic>#772f1):
A non-null String must be provided to a Text widget.
'package:flutter/src/widgets/text.dart':
Failed assertion: line 298 pos 10: 'data != null'
The relevant error-causing widget was:
FutureBuilder<dynamic> file:///Users/nossu3751/Downloads/flutter_project/moim_app/lib/user/profile.dart:39:12
When the exception was thrown, this was the stack:
#2 new Text (package:flutter/src/widgets/text.dart:298:10)
#3 UserPageState.userScaffold (package:moimapp/user/profile.dart:26:18)
#4 UserPageState.build.<anonymous closure> (package:moimapp/user/profile.dart:41:54)
#5 _FutureBuilderState.build (package:flutter/src/widgets/async.dart:732:55)
#6 StatefulElement.build (package:flutter/src/widgets/framework.dart:4619:28)
More exactly, it says that the userName that I'm trying to use in the FutureBuilder is null, even though I believe I assigned the value already through running _infoInit() method and it does actually show correctly on the simulator.
I would really appreciate it if someone can let me know what I'm doing wrong here, and what I can do to stop this message from appearing again. Thank you very much in advance!
The problem is that the FutureBuilder's builder method is invoked each time the AsyncSnapshot is changed (and, initially, the snapshot has no data). Therefore, in the first couple of times that builder is called, userName will be null, thus giving you that error; but after some point, the username will have been fetched, and when the builder function is called you'll see the username on the screen correctly.
The idiomatic way to use FutureBuilder is the following:
FutureBuilder(
future: myFuture,
builder: (context, AsyncSnapshot snapshot) {
// Try adding this print to your code to see when this method is being executed!
print('Building with snapshot = $snapshot');
if (snapshot.hasData) {
// return widget with data - in your case, userScaffold
}
else if (snapshot.hasError) {
// return widget informing of error
}
else {
// still loading, return loading widget (for example, a CircularProgressIndicator)
}
},
);
So, initially, the builder function will be called will be with a snapshot without data (the "else" branch). You'll probably want to show a loading widget in that case. Then, after some time, the Future completes, and the builder function is called with a snapshot having either the data or some error.
Another important thing in your code is that your function _infoInit doesn't actually return anything. So, in fact, your FutureBuilder isn't using the data from the AsyncSnapshot (which means that the above snippet will not actually work, since snapshot.hasData will never be true). With FutureBuilder, you normally want to build the widget using the data returned by the AsyncSnapshot. Instead, what happens in your code is:
FutureBuilder is created. This calls _infoInit(), which triggers fetching the data from Firebase;
FutureBuilder's builder method is called. It tries using userName, but it is null so Flutter shows the failed assertion;
_infoInit() fetches all the data, and returns a Future (this future is returned automagically because of the async clause in the method signature; however, without a return clause, it doesn't actually return any data). But despite the Future not having any data, the 3 variables in the state (including userName) have been updated, and now contain some data.
Since the future passed to the FutureBuilder has completed, the builder method is called again. This time, userName has data, so it builds correctly.
It's fine to write the code as you did, but in that case, you don't need to use a FutureBuilder. You could just invoke _infoInit() from the widget's initState() method (initState is a method called once when the State is first built) and, after the data is fetched, call setState(). Here's how that would look:
class UserPage extends StatefulWidget {
#override
UserPageState createState() => UserPageState();
}
class UserPageState extends State<UserPage> {
String userName;
String userEmail;
String collegeName;
bool loadingData = true;
#override
void initState() {
_infoInit();
}
// This can be void now, since we're changing the state, rather than returning a Future
void _infoInit() async {
String userName = await HelperFunctions.getUserNamePreference();
String userEmail = await HelperFunctions.getUserEmailPreference();
String collegeName = await HelperFunctions.getUserCollegePreference();
setState(() {
// In theory, we could have just updated the state variables above, but the
// recommended practice is to update state variables inside setState.
this.userName = userName;
this.userEmail = userEmail;
this.collegeName = collegeName;
loadingData = false;
});
}
Widget userScaffold(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(userName),
backgroundColor: Colors.lightBlue,
),
body: Center(child: Text("This is User Page")));
}
#override
Widget build(BuildContext context) {
if (loadingData) {
// We don't have the data yet, so return a widget to indicate some loading state
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return userScaffold(context);
}
}
The above snippet doesn't treat for errors in fetching the data, which you'll probably want to do. And to do that, you might use a flag called 'hasError' or something - which ultimately, will give very similar code to how the "idiomatic" FutureBuilder builder method is written.
Both are valid approaches; FutureBuilder maybe uses less code (and might be simpler to use if the rest of your code already uses Futures), but ultimately, it's up to your preference.
You need to use ConnectionState inside your builder. Look at this code template: (Currently your builder return userScaffold without waiting for the future to complete)
return FutureBuilder(
future: yourFuture(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// future complete
// if error or data is false return error widget
if (snapshot.hasError) {
return _buildErrorWidget('SOMETHING WENT WRONG, TAP TO RELOAD');
}
// return data widget
return _buildDataWidget();
// return loading widget while connection state is active
} else
return _buildLoadingWidget();
},
);

Not Loading the value in Real Time

I am using below code to get the total number of documents from the firestore collection and then store it to int countDocument, it does get the value accurately, but when I used to display the same value in the Flutter widget Text Widget which is nested in Scaffold, upon loading the screen it does not show the value, showing null only, only upon hot reload it shows the value on the screen.
To represent the value of countDocument in Text Widget, I did countDocument.toString()' but still it does not show the value upon initial loading of the screen
How should I resolve it?
void countDocuments() async {
StreamSubscription<QuerySnapshot> _myDoc = await Firestore.instance.collection('users').snapshots().listen((result) {
countDocument = result.documents.length;
print(countDocument);
});
You need to use a StatefulWidget, then using setState, you can change the data of the Text:
void countDocuments() async {
StreamSubscription<QuerySnapshot> _myDoc = await Firestore.instance
.collection('users')
.snapshots()
.listen((result) {
setState(() {
countDocument = result.documents.length;
});
print(countDocument);
});
}
setState will call the build method again with the new data. Inside the build method you can do the following:
Text(countDocument ?? "loading")

Flutter, Is it possible to use Firestore Stream Builder in function without a widget build method?

I have a function that gets the results of a google places search for stores and searches Firestore to see if the store is already in the database.
Whenever I run it thought the Stream Builder is doing nothing.
I think the problem is that the function is not within a widget and does not have a build method. Here is the code:
void searchStores() async {
Prediction newStore = await PlacesAutocomplete.show(
context: context,
apiKey: kGoogleApiKey,
mode: Mode.overlay,
language: "en",
components: [new Component(Component.country, "au")]);
await places.getDetailsByPlaceId(newStore.placeId).then((detailStoreInfo) {
print('running 1');
StreamBuilder (
stream: Firestore.instance.collection('stores').document(detailStoreInfo.result.id).snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot){
print('running 2');
setState(() {
if (snapshot.connectionState == ConnectionState.active &&
snapshot.hasData){
print('running 3');
if (snapshot.data['veganOnly'] == null || snapshot.data['veganOnly'] == false){
print('running 4');
setState(() {
firstStore = true;
});
}
}
});
return null;
}
);
});
}
As you can see I added print statements to work out where my code is failing. 'running 1' is show so the places is returning a response.
But none of the other statements (2,3 or 4) are printing so it seems the stream builder is not working, not even coming back with null values.
I also tried putting a return before the stream builder but that had no effect either.
Am I correct in thinking this because it is not in the build method of a widget or is it something else entirely.
Thanks
A StreamBuilder is a Widget and as such, has to be inserted somewhere in your widget tree, just as you would for a Text widget. Just pass it a stream and return another widget inside it’s builder callback
You need to use the Stream class for it, for instance to get all details about a collection named "users", you can use,
Stream<QuerySnapshot> stream = _db.collection("users").snapshots();
stream.forEach((QuerySnapshot element) {
if(element == null)
return;
for(int count=0;count<element.documents.length;count++) {
print(element.documents[count].data.toString());
}
});

How to read data from firebase inside AlertDialog widget?

I am trying to read data from firebase inside an AlertDialog in flutter, when a button is pressed, and then update it afterwards.
I have tried using a StreamBuilder, but nothing happens
new FlatButton(
child: const Text('+ Add'),
onPressed: () {
StreamBuilder(
stream: Firestore.instance.collection('users').document(user.uid).collection('Filtre').document('ChooseSelf').snapshots(),
builder: (context, snapshot) {
var TypeSelfFilters = snapshot.data;
List<String> ListOfTypeSelf = List.from(TypeSelfFilters["Personer"]);
ListOfTypeSelf.add("value of TextFormField");
Firestore.instance.collection('users').document(user.uid).collection('Filtre').document('ChooseSelf').updateData({'Personer': ListOfTypeSelf});
}
);
Navigator.pop(context);
}
);
I do not get any errors, but the code inside the StreamBuilder is not executed for some reason.
Thank You
Hm... It looks to me that you are expecting to get the data when the use taps on FlatButton.
Let's look what happens:
tap on FlatButton
Instantiate a StreamBuilder
Start getting data from Firestore
Do some Firestore magic, update date
Then close dialog by navigator.pop()
Problem: you call navigator.pop() right after Instantiation of StreamBuilder. StreamBuilder has to wait somewhat to get the data. If you pop a route, and with that destroying your alert dialog, the builder callback will not be called. So the actual sequence of things happening is: Tap -> Instantiate StreamBuilder -> pop route
Recommendation: why wrap your computation in a StreamBuilder? You could do:
onPressed: () {
Firestore.instance.collection('users')/*...*/.snapshots().then((snapshot) async {
// then branch is executed once snapshot is retrieved from firestore
var TypeSelfFilters = snapshot.data;
// do some more computation and magic
await Firestore.instance.collection/*...*/.updateData();
// wait for updateData to finish
Navigator.pop(context); // this context is not the context inside the StreamBuilder
});
}
Thanks to Daniel V. i found a solution:
var myData = Firestore.instance.collection('users').document(user.uid).collection('Filtre').document('ChooseSelf').snapshots().first;
myData.then((snapshot) async {
var TypeSelfFilters = snapshot.data;
List<String> ListOfTypeSelf = List.from(TypeSelfFilters["Personer"]);
ListOfTypeSelf.add("bare en test");
Firestore.instance.collection('users').document(user.uid).collection('Filtre').document('ChooseSelf').updateData({'Personer': ListOfTypeSelf});
Navigator.pop(context); // this context is not the context inside the StreamBuilder
});
}
)

Resources