How to properly wait until future is complete in dart - asynchronous

There is a slight bug in my app made with Flutter, that when the user has signed in, it fetches the user information from my database but not fast enough and causes a visual error on my front end of the app. The app has layouts that use the user information (name, location, and image) and it is not being loaded quick enough. I was wondering if there is a way to wait for my future to complete and once it is done, it can navigate the user to the front end with no problem.

You Should fetch your date from the database in the initState() function, then you have to modify your widget builder to use FutureBuilder, here's an example:
Widget build(BuildContext context) {
return FutureBuilder(
future: getProfile(),
builder: (BuildContext context, AsyncSnapshot<SharedPreferences> snapshot) {
if(snapshot.connectionState == ConnectionState.done){
return new MaterialApp();
}
}
)
}
note that you can replace AsyncSnapshot<SharedPreferences> with the type your Future function returns.

Related

Flutter Update Text Widget Variable without Reloading

Im using Flutter with Firestore to make Investing Application.
enter image description here
In this page, I get Firestore's data in Streambuilder.
The data what i recieved are displayed in Listview with Inkwell.
What I want to do is when I click the Inkwell, then change the bottom left's Text Widget's text into Listview's number.
So, I used the setstate method with FutureBuilder.
It works but there are some problems.
When I click the Inkwell, then whole page reloads.
So, My Streambuilder widgets displays circleprogressindicator short.
However I think this is not good for users.
I want to solve this problem.
Is there any solution?
My code is in here.
I'm sorry for my poor English and I hope my problem was properly communicated.
https://github.com/RGLie/Investing-Game-ICISTS/blob/main/lib/startup_1_trade.dart
You can fix the rebuilds by storing the streams in variables outside of the build method and using them in the StreamBuilder widgets.
class Startup1Trade extends StatefulWidget {
...
}
class _Startup1TradeState extends State<Startup1Trade> {
...
CollectionReference prices = FirebaseFirestore.instance.collection('startup_1');
CollectionReference users = FirebaseFirestore.instance.collection('users');
final Stream priceStream = prices.doc('price').snapshots();
final Stream userStream = users.doc(widget.user.uid).snapshots();
#override
Widget build(BuildContext context) {
...
}
}
Use them like this:
StreamBuilder<DocumentSnapshot>(
stream: priceStream,
builder: (context, snap) {
...
}
}
StreamBuilder<DocumentSnapshot>(
stream: userStream,
builder: (context, snap) {
...
}
}

What is the difference between existing types of snapshots in Firebase?

as I'm further progressing with my code and my coding experiences in Flutter. I've encountered different types of snapshots, while working with the Firebase API. I'm talking about AsyncSnapshots, QuerySnapshots, DocumentSnapshots and DataSnapshots. If there are more pls name them too.
I was wondering, what the exact differences are between those snapshots.
What I've figured so far is that an AsyncSnapshot is probably a Snapshot that is taken asynchronously, meaning the Widget is built before the Data of the Snapshot is available therefore making it async (pls correct me if I'm wrong). Here is where my confussion starts, what exactly is a Snapshot? And what is the "Data" in each of them.
For example: Why can't the same function retrieve the needed Data in all of the Snapshots, but is only functioning on a particular Snapshot.
Why is it necessary to convert the Data from a QuerySnapshot to a DocumentSnapshot, to make it accesible (Again pls correct me if I'm wrong)?
And what is the exact difference between DocumentSnapshot and DataSnapshot. And why are they called differently, when both of them return Maps?.
Thank you in advance.
As far as I can tell you are asking this in the context of Flutter, so I'll answer for that below.
There are two databases in Firebase: the original Realtime Database, and the newer Cloud Firestore. Both are equally valid options today, but they are completely separate with their own API. But both return snapshots of data, where the snapshot is a copy of the data from the database in your application code.
In Flutter you have FutureBuilder and StreamBuilder, which deal with snapshots of data that is loaded asynchronously.
Let's see if I can cover them:
An AsyncSnapshot is Flutter's wrappers around data from asynchronous data sources, such as Firestore and Realtime Database. They cover the states that such data can be in, from the initial connection, through retrieval, until errors or having the data.
DocumentSnapshots and QuerySnapshots are Firestore's classes to either represent a single document, or a list of documents that you get when reading from the database. So if you load a single document, you get a DocumentSnapshot with its data. And if you load a list of document, you get a QuerySnapshot that you then loop over to access the individual DocumentSnapshots.
A DataSnapshot is the Realtime Database's class for both a single Node, and a list of nodes from the database.
So in Flutter you'll have an AsyncSnapshot that refers to one of the Firebase snapshot classes, and that Firebase snapshot then wraps the actual data.
Say you want to display a list with the documents in a collection from Firestore, you'll have:
An AsyncSnapshot to feed to your StreamBuilder, so that it can render the correct state of data loading.
A QuerySnapshot for the list of documents from the database.
Each item in that list is then a DocumentSnapshot with a snapshot of the data from a single document.
I actually find this much easier to see in code, as in this example from the FlutterFire documentation:
class UserInformation extends StatelessWidget {
#override
Widget build(BuildContext context) {
CollectionReference users = FirebaseFirestore.instance.collection('users');
return StreamBuilder<QuerySnapshot>(
stream: users.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document.data()['full_name']),
subtitle: new Text(document.data()['company']),
);
}).toList(),
);
},
);
}
}

How can I load the previous signed in user?

In my application, I happen to have two types of users, hospital, and patients, both are configured to use the same authentication by firebase. What I would like to know is, how do I load the page for the respective user depending on how they were signed in previously? For example, if a hospital had signed in and not logged out, when they run the app, it should display the hospitals' dashboard and not the patients'. How do I configure this?
shared_prefs supports Flutter web. Save a value stating the account type.
See this answer:
https://stackoverflow.com/a/59579821/13714686
SharedPreferences prefs = await SharedPreferences.getInstance();
bool isHospital = (prefs.getBool('isHospitalAccount');
if(isHospital == true){
//hospital navigation
}
else{
//patient navigation
}
First, you need to check whether the user is logged in or not. For this, you can check through the below code.
class MyApp extends StatelessWidget{
#override
Widget build(BuildContext context){
return FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot){
if (snapshot.hasData){
FirebaseUser user = snapshot.data; // this is your user
/// is because there is a user already logged, So route them to a Screen of Dashboard directly.
return MainScreen();
}
/// other way there is no user logged.
return LoginScreen();
}
);
}
}
Now, you need to maintain a list of users in FireStore where the user's metadata like the last login, User Type information will be stored. Using this information you can Route the user accordingly.

Run Firebase Cloud Function before page load Flutter

I have a Firebase Cloud Function that creates a document when a new user signs up. The document that gets created by the function is where the user data will be stored. The process is as such:
User signs up
User document created in Firestore
Firebase Function triggered to create 'other' document
User sees homepage
Homepage uses data from 'other' document
The problem I have is the user is going straight to the homepage before the Firebase Function is executed and the 'other' document is not created yet.
This means the user is just seeing a CircularProgressIndicator because the page is loading before the 'other' document exists.
It works fine if the user clicks away from that page and returns to it, because by that time the 'other' document exists. Likewise, when I add a 5 second delay on initially loading the homepage, it works because the Firebase Function has time to execute - but this is not a nice solution.
I am wondering how I can ensure the Firebase Function has executed and the 'other' document created before loading the homepage?
initState
void initState() {
super.initState();
final user = Provider.of<UserClass>(
context,
listen: false);
final uid = user.uid;
_houseID = getHouseID(uid);
}
Future returning ID of document created by Firebase Function
Future<String> getHouseID(uid) async {
String houseID;
await Future.delayed(Duration(milliseconds: 5000)); // with this delay it works fine
await FirebaseFirestore.instance
.collection('users')
.doc(uid)
.collection('userHouses') // this collection is being created by a Cloud Function
.get()
.then(
(value) {
houseID = value.docs.single.id;
},
);
return houseID;
}
FutureBuilder
return FutureBuilder(
future: _houseID,
builder: (BuildContext context, AsyncSnapshot snapshot) {
hhid = snapshot.data;
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator()); // this runs forever when the user first signs up
} else {
return // homepage using hhid to retrieve user data
You can open a stream which listens to that specific document after the user signs up. The stream initially may be empty, so you can check if the document exists. Once the document is written, the stream will be updated and then you can close it if you're done.
here's a simple code that explains the idea:
final subscription = FirebaseFirestore.instance.doc('path-to-document').snapshots().listen((event) {
if (event.exists) {
// do something with the data
final data = event.data();
// update your state
// .... some code
// call a function to close the subscription if you don't need it
closeSubscription();
}
});
closeSubscription() {
subscription.cancel();
}

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();
},
);

Resources