How to inform FutureBuilder that database was updated? - flutter

I have a group profile page, where a user can change the description of a group. He clicks on the description, gets on a new screen and saves it to Firestore. He then get's back via Navigator.pop(context) to the group profile page which lists all elements via FutureBuilder.
First, I had the database request for my FutureBuilder inside the main build method (directly inside future builder 'future: request') which was working but I learnt it is wrong. But now I have to wait for a rebuild to see changes. How do I tell FutureBuilder that there is a data update?
I am loading Firestore data as follows within the group profile page:
Future<DocumentSnapshot> _future;
#override
void initState() {
super.initState();
_getFiretoreData();
}
Future<void> _getFiretoreData() async{
setState(() {
this._future = Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();});
}
The FutureBuilder is inside the main build method and gets the 'already loaded' future like this:
FutureBuilder(future: _future, ...)
Now I would like to tell him: a change happened to _future, please rebuild ;-).

Ok, I managed it like this (which took me only a few lines of code). Leave the code as it is and get a true callback from the navigator to know that there was a change on the second page:
// check if second page callback is true
bool _changed = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileUpdate(userId: globals.userId.toString())),
);
// if it's true, reload future data
_changed ? _getFiretoreData() : Container();
On the second page give the save button a Navigator.pop(context, true).

i would advice you not to use future builder in this situation and use future.then() in an async function and after you get your data update the build without using future builder..!
Future getData() async {
//here you can call the function and handle the output(return value) as result
getFiretoreData().then((result) {
// print(result);
setState(() {
//handle your result here.
//update build here.
});
});
}

How about this?
#override
Widget build(BuildContext context) {
if (_future == null) {
// show loading indicator while waiting for data
return Center(child: CircularProgressIndicator());
} else {
return YourWidget();
}
}

You do not need to set any state. You just need to return your collection of users in your GetFirestoreData method.
Future<TypeYouReturning> _getFirestoreData() async{
return Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();
}
Inside your FutureBuilder widget you can set it up something like Theo recommended, I would do something like this
return FutureBuilder(
future: _getFirestoreData(),
builder: (context, AsyncSnapshot<TypeYouReturning> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.length == 0)
return Text("No available data just yet");
return Container();//This should be the desire widget you want the user to see
}
},
);

Why don't you use Stream builder instead of Future builder?
StreamBuilder(stream: _future, ...)
You can change the variable name to _stream for clarity.

Related

When I call the function in Text Widget, I get this display in my screen "instance of Future dynamic"?

This is the function, I want to retrieve currentUser data.
getData() async {
User? user = await FirebaseAuth.instance.currentUser;
print(user?.displayName);
}
How to display the name on the screen
When I call the function in text widget i.e
Text(getData().toString()),
I get the following display Instance of 'Future'<'dynamic'>''
i'm a beginner in Flutter, please help!
Since the function getData is async, it's a Future and you can't use a future methods inside your tree widget directly
you can use it inside your widget tree using the widget FutureBuilder
FutureBuilder(
future: getData(),
builder: (context, snapshot){
if (!snapshot.hasData) return const SizedBox();
return Text(snapshot.data?.toString() ?? '');
}
also, you have to modify your method to make it return something,
Ex.:
getData() async {
User? user = await FirebaseAuth.instance.currentUser;
print(user?.displayName);
return user?.displayName;
}
UPDATE:
to access all the info you want from the User object, let your method return the whole object;
getData() async {
User? user = await FirebaseAuth.instance.currentUser;
print(user?.displayName);
return user;
}
and your FutureBuilder will be
FutureBuilder(
future: getData(),
builder: (context, snapshot){
if (!snapshot.hasData) return const SizedBox();
if (snapshot.data == null) return const Text('Current user is null');
return Column(
children: [
Text('Name: ${snapshot.data?.displayName}'),
Text('Email: ${snapshot.data?.email}'),
///Add any attribute you want to show..
]
);
}
getData() async {
User? user = await FirebaseAuth.instance.currentUser;
if(user!=null){
print(user.displayName);
print(user.email);}
}
this will wait for an async method to first complete then print

Displaying multiple screens based on ternary condition in dart

I have a scenario where I am showing a spinner when a page loads and while it's loading, it fetches some data in DB and sets a bool value to either true or false based on data availability.
I then want to share either screen A or B based on the boolean result.
I have done the following in my code but the app keeps showing the spinner. Any ideas what I might be doing incorrectly?
return _isLoading?
Center(child:Loading(),):
_isPersonalInfoSubmitted?ScreenA():ScreenB();
Second Attempt (Using Future Builder)
I want to show categories if the loggedin user is an admin. Else, for the rest of the users, I want to fetch address of the user from the DB. If the address is null, show Personal Details Screen else show Categories.
return FutureBuilder (
future: userId=='ADMIN_ID'?
Provider.of<Categories>(context,listen:false).fetchAndReturnCategories():
Provider.of<Addresses>(context,listen: false).fetchAndReturnAddress(userId)!=null?
Provider.of<Categories>(context,listen:false).fetchAndReturnCategories():null,
builder: (context, snap) {
inspect(snap);
if (snap.hasData) {
var categoriesData = Provider.of<Categories>(context);
return snap.hasData?
Scaffold(...) : PersonalDetails();
What happens here is that the method fetchAndReturnCategories gets executed even if the userID is not admin id. Do I have the correct setup?
prefer to use FutureBuilder
FutureBuilder<SomeClass>(
future: fetchdatFuture,
builder: (ctx, snap) {
if (snap.hasData) {
return snap.data?ScreenA():ScreenB();
} else if (snap.connectionState == ConnectionState.waiting)
{
return Center(child:Loading());
}
return Text("Error");
},
)
I think you have missed the setState to rebuild your widget
var bool _isLoading = true;
return _isLoading?
Center(child:Loading(),):
_isPersonalInfoSubmitted?ScreenA():ScreenB();
void _apiCall() {
// After Success of API Call
setState((){
_isLoading = false;
})
}

Fixing Issues with FutureBuilder

In my Flutter project, I am trying to implement a button click event by using FutureBuilder. Basically when the button clicked, it supposed to get the data and display in a table. So my button onPressed event handling is as below:
onPressed: () async{
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
var p = double.parse(loanAmount);
var r = double.parse(interestRate);
var n = int.parse(monthes);
Api api = new Api();
new FutureBuilder<List>(
future: api.calculateEmi(p, r, n),
builder: (BuildContext buildContext, AsyncSnapshot<List> snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
print( snapshot.data);
return new SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
child: DataTableWidget(listOfColumns: snapshot.data.map(
(e)=>{
'Month': e['Month'],
'Principal': e['Principal'],
'InterestP': e['InterestP'],
'PrinciplaP': e['PrinciplaP'],
'RemainP': e['RemainP']
}).toList()
),
);
}
}
);
}
}
The Api call is working and the method calculateEmi is called and get data returned ( a List of Map), but the view just not updated and no table appeared at all, and I use breakpoint at the builder portion but it never go into it, where did I do wrong, can anyone help? thanks.
The FutureBuilder needs to be inserted somewhere in the flutter widget tree. Simply creating a new FutureBuilder doesn't tell flutter what to do with it.
I'm guessing you instead want to put the FutureBuilder you created somewhere in the parent widget of the onPressed method. If you need it to only show when the button is pressed you can do that with a bool that determines whether to show the FutureBuilder or not.
Ex.
Widget build(context) {
if(buttonPressed) {
return FutureBuilder(
...
);
}
else {
return Container();
}
}

Flutter: Get specific data from firestore and output it in a text

I'm new to flutter / dart. I need to get a specific data in firestore and output it in a text, Ex: Collection - Document(currentUID) - Field(Name). I need to get that data, the "Name" and set it as my AppBar name.
Here is the code that I've done so far. Need help badly thank you.
I have manage to print the name in the terminal but I cant set it as my AppBar name.
_loadcurrentName() async{
await Firestore.instance.collection('USER').document(currentUser.uid).get().then((DocumentSnapshot ds) async {
var name = ds['Name'];
print(name);
});
}
Make the _loadcurrentName() method return a Future<String>, then you could use a FutureBuilder as the title of the AppBar and handle the cases if you didn't receive it yet or if the request fail.
To avoid showing every time "Loading..." or a "Default Title", you could save the last received title on the SharedPreferences and show that instead and then update it with the new one received from the request.
Future<String> _title;
#override
void initState() {
super.initState();
_title = _loadcurrentName();
}
Future<String> _loadcurrentName() async {
await Firestore.instance.collection('USER').document(currentUser.uid).get().then((DocumentSnapshot ds) async {
return ds['Name'];
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: FutureBuilder(
future: _title,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('${snapshot.data}');
} else if (snapshot.hasError) {
return Text('Default Title');
}
return Text('Loading...');
},
),
),
);
}
modify your method like this:
Future<String> _getAppBarNameWidget() async{
await Firestore.instance.collection('USER').document(currentUser.uid).get().then((DocumentSnapshot ds) async {
var name = ds['Name'];
return name;
});
}
and somwhere in your scaffold code, do this:
return Scaffold(
appBar: AppBar(
title: FutureBuilder(
future: _getAppBarNameWidget(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return Text(snapshot.data);
}),
actions: <Widget>[AppBarNotificationAction()],
)
);
But since your method is async, you will definitely have the Scaffold appear first without title and then a few millisecs later, the FutureBuilder will return with a title and you will see it on the screen. Most likely not what you want. I would load this stuff at the start of your application, save the text somewhere where your scaffold can access it.
Furthermore every time your scaffold rebuilds, you will make a DB call and reload the title again. Since i assume title is more or less static, this is a no go as well. But i hope this helps you starting.

Infinite loop on using FutureBuilder with API call

I am trying to populate my ListView with the result from an API. The API call must take place after the values have been retrieved from Shared Preference. However on execution my function for API call runs an infinite loop and the UI doesn't render. I tracked this behaviour through debug statements.
The circular indicator that should be shown when Future builder is building UI is also not showing.
How can I resolve this?
My code:
class _MyHomePageState extends State<MyHomePage>{
#override MyHomePage get widget => super.widget;
String userID = "";
String authID = "";
//Retrieving values from Shared Preferences
Future<List<String>> loadData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> l= new List<String>();
if(prefs.getString("ID") == null){
l.add("null");
}
else{
l.add(prefs.getString("ID"));
}
if(prefs.getString("authID") == null){
l.add("null");
}
else{
l.add(prefs.getString("authID"));
}
return l;
}
//Setting values retrieved from Shared Pref
setData() async{
await loadData().then((value) {
setState(() {
userID = value[0];
print('the user ID is' + userID);
authID = value[1];
print('the authID is' + authID);
});
// getAllTasks(userID, authID);
});
print("Set data execution completed ");
}
//FUNCTION to use values from Shared Pref and make API Call
Future<List<Task>> getAllTasks() async{
await setData();
//Waiting for Set Data to complete
print('Ive have retrived the values ' + userID + authID );
List<Task> taskList;
await getTasks(userID, authID, "for_me").then((value){
final json = value;
if(json!="Error"){
Tasks tasks = tasksFromJson(json); //of Class Tasks
taskList = tasks.tasks; //getting the list of tasks from class
}
});
if(taskList != null) return taskList;
else {
print('Tasklist was null ');
throw new Exception('Failed to load data ');
}
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context){
_signedOut(){
widget.onSignedOut();
}
//To CREATE LIST VIEW
Widget createTasksListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values == null ? 0 : values.length,
itemBuilder: (BuildContext context, int index) {
return values.isNotEmpty ? Ink(....
) : CircularProgressIndicator();
},
);
}
//MY COLUMN VIEW
Column cardsView = Column(
children: <Widget>[
....
Expanded(
child: FutureBuilder(
future: getAllTasks(),
initialData: [],
builder: (context, snapshot) {
return createTasksListView(context, snapshot);
}),
),
],
);
return Scaffold(
body: cardsView,
);
}
}
Instead of being called once... my setData function is being called repeatedly.. How can I resolve this..please help
You're creating Future object on every rebuild of the widget. And since you're calling setState inside your setData method, it triggers a rebuild recursively.
To solve this problem you have to keep a reference to the Future object. And use that reference for the FutureBuilder then it can understand that it is the previously used one.
E.g:
Future<List<Task>> _tasks;
#override
void initState() {
_tasks = getAllTasks();
super.initState();
}
And in your widget tree use it like that:
Expanded(
child: FutureBuilder(
future: _tasks,
initialData: [],
builder: (context, snapshot) {
return createTasksListView(context, snapshot);
}),
),
The FutureBuilder widget that Flutter provides us to create widgets based on the state of some future, keeps re-firing that future every time a rebuild happens!
Every time we call setState, the FutureBuilder goes through its whole life-cycle again!
One option is Memoization:
Memoization is, in simple terms, caching the return value of a function, and reusing it when that function is called again.
Memoization is mostly used in functional languages, where functions are deterministic (they always return the same output for the same inputs), but we can use simple memoization for our problem here, to make sure the FutureBuilder always receives the same future instance.
To do that, we will use Dart’s AsyncMemoizer.
This memoizer does exactly what we want! It takes an asynchronous function, calls it the first time it is called, and caches its result. For all subsequent calls to the function, the memoizer returns the same previously calculated future.
Thus, to solve our problem, we start by creating an instance of AsyncMemoizer in our widget:
final AsyncMemoizer _memoizer = AsyncMemoizer();
Note: you shouldn’t instantiate the memoizer inside a StatelessWidget, because Flutter disposes of StatelessWidgets at every rebuild, which basically beats the purpose. You should instantiate it either in a StatefulWidget, or somewhere where it can persist.
Afterwards, we will modify our _fetchData function to use that memoizer:
_fetchData() {
return this._memoizer.runOnce(() async {
await Future.delayed(Duration(seconds: 2));
return 'REMOTE DATA';
});
}
Note: you must wrap inside runOnce() only the body, not the funciton call
Special thanks to AbdulRahman AlHamali.
You need to save the Future in the State because doing getAllTasks() is triggering the call on every build callback.
In the initState:
this.getAllTasksFuture = getAllTasks();
Then you would use this Future property in the FutureBuilder.