How to update future of future builder - flutter

Say I have a FutureBuilder and its future is getBusinessListByRating(), which makes a call to an API to get a BusinessList sorted by Rating. Then I press a button to sort the list alphabetically. When I do this, I want the future to be getBusinessListByAlpha() and to rerender.
How can I achieve this?
I tried using setState to update the future but it does not work.

In your state class you should initialize the future like this:
Future getBusinessListFuture;
#override
void initState() {
super.initState();
getBusinessListFuture = getBusinessListByRating(rating);
}
and in your FutureBuilder:
FutureBuilder(
future: getBusinessListFuture,
builder: ....
)
to update future on button click:
onTap: () {
setState(() {
getBusinessListFuture = getBusinessListByRating(alphabetical);
});
}
This should update the future and rebuild the widget.

How about having a method that takes an argument instead, and just alter the argument. Then let that function call your specific secondary methods.
Something like:
enum BusinessListOrdering {
rating,
alpha,
score
}
getBusinessListBy(BusinessListOrdering order) {
switch (order) {
case BusinessListOrdering.rating:
return getBusinessListByRating();
case BusinessListOrdering.alpha:
return getBusinessListByAlpha();
case BusinessListOrdering.score:
return getBusinessListByScore();
default:
return getBusinessListByRating();
}
}
So when you press the button, you set the state for the enum variable in your widget.
onPressed: () {
setState(() {
order = BusinessListOrdering.alpha;
});
},
and the FutureBuilder instead calls:
getBusinessListBy(order)

Related

Flutter - Implement an async function from showBottomSheet

I have a function that call showModalBottomSheet which I need to remove the barrier.
Future<..> popup() async => await showModalBottomSheet<..>(
...
builder : (BuildContext context){
...
Navigator.of(context).pop(..);
...
}
);
I have two options. Since I don't want to use different Scaffold, that leaves me to use showBottomSheet instead. But I also want to keep my function as async because I need the BottomSheet to return a value on it's dismiss. showBottomSheet returns PersistentBottomSheetController not a Future. I have no idea how to implement a Future returning function from it, the way I did with showModalBottomSheet.
Future<..> popup() async{
???
Scaffold.of(context).showBottomSheet(..)<..>(
(BuildContext context){
...
Navigator.of(context).pop(..);
...
}
);
???
}
Greatly appreciate the help. Thank you.
You can't return Future from showModalBottomSheet instead
you can use isClosed to check if bottom sheet closed and this getter return future.
you can use it like:
Future<bool> popup(BuildContext context) async{
PersistentBottomSheetController myModal = showBottomSheet(
context: context,
builder: (context) {
return const Text("Modal");
},
);
Future<bool> isClosed = await myModal.closed;
return isClosed;
}

Proper use of cubit

I am still learning how to use cubits and blocs, and I am trying to use a cubit in my project, but I got a little bit confused about how to use it.
There is a screen that requires a phone number and I use the lib "intl_phone_number_input" to format, validate and select the country. When I click the button to the next page it needs to check if the phone is valid, but I need to have a variable that stores this info. The widget InternationalPhoneNumberInput has a property onInputValidated that returns true if the phone number is valid, so where should I create this variable? Should I create it in my widget class or inside the cubit? I created it inside cubit but I am not sure if it is the correct way, so I got this:
onInputValidated: (bool value) {
BlocProvider.of<LoginCubit>(context).isValid =
value;
},
I've studied and seen some examples about cubits and how to use'em but I still didn't get it at all, because in the examples the cubit never used a variable, all variables became a state, but in my case, I need the value as a variable.
I am confused too about how to show a dialog using cubit, I've done it this way:
#override
Widget build(BuildContext context) {
return BlocConsumer<LoginCubit, LoginState>(
listenWhen: (previous, current) => current is ShowDialogErrorLoginState || current is NavigateFromLoginStateToHomePageState,
listener: (context, state) {
if (state is ShowDialogErrorLoginState) {
showErrorDialog(context, state.titleMessage, state.bodyMessage);
}
if (state is NavigateFromLoginStateToHomePageState) {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const MyHomePage()));
}
},
builder: (context, state) {
if (state is ShowLoginState) {
return buildPhoneForm(context);
}
if (state is SendingCodeLoginState) {
return ProgressView(message: 'Sending SMS code',);
}
if (state is ShowCodeLoginState) {
return buildCodeForm(context);
}
return const ErrorView('Unknown error');
},
);
}
and in my cubit I did the following:
void goToCodeVerification(String phoneNumber) async {
if (!isValid){
String titleMessage = "Phone number invalid";
String bodyMessage = "The given phone number is invalid";
emit(ShowDialogErrorLoginState(titleMessage, bodyMessage));
emit(ShowLoginState());
} else {
emit(SendingCodeLoginState());
// TO DO
// use API to send a code
emit(ShowCodeLoginState());
}
}
Is this the correct way to show a dialog with a cubit?
Ok, so you have a value you want to use, the variable doesn't affect state, and you need to access it inside your cubit.
for something like this, I think storing the variable on the cubit makes the most sense, but keep in mind either approach is acceptable for such a simple case.
I also don't really like how the below code looks:
onInputValidated: (bool value) {
BlocProvider.of<LoginCubit>(context).isValid =
value;
},
it is a bit clunky, I would prefer to move the whole callback into the cubit:
void onInputValidated(bool value) => isValid = value;
that way:
final cubit = BlocProvider.of<LoginCubit>(context);
...
onInputValidated: cubit.onInputValidated,

Change state in FutureBuilder when the data is loaded

How can I update the UI if I need to wait for the FutureBuilder? Do I need to call my future function twice, one for for the builder and one again to change the UI?
FutureBuilder<String>(
future: getUserOrder(4045),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data,style: Theme.of(context).textTheme.headline);
} else if (snapshot.hasError) {
// I need to change the state at this point
return Text("${snapshot.error}",style: Theme.of(context).textTheme.headline);
} else {
return CircularProgressIndicator();
}
}),
Calling setState inside the FutureBuilder throws this error:
setState() or markNeedsBuild() called during build.
I don't need to display a button or any other other to be clicked. I want to perform the action automatically when the date is loaded in the futureBuilder
Since I couldn't call setState inside FutureBuilder the solution was remove it and do something like this:
getBillingInfo() {
Provider.of<MyRents>(context, listen: false)
.getBillingInfo(context)
.then((billingInfo) {
setState(() {
if (billingInfo["companyInfo"] != null &&
billingInfo["taxes"].isNotEmpty) {
_canGenerateInvoices = true;
} else {
_canGenerateInvoices = false;
}
});
});
}
...
void initState() {
super.initState();
getBillingInfo();
}
...
Visibility(
visible: _canGenerateInvoices,
child: MyWidget()
)
Having this, when I perform other actions I can always change the value of _canGenerateInvoices

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.

How to inform FutureBuilder that database was updated?

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.