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();
}
}
Related
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
I'm having some troubles with the bloc pattern, specifically with BlocConsumer reactions to the emitted states which lead to certain overlaps.
The main character is an object list, for semplicity let's say an object with a String and int parameters.
On the first page I'm adding an event to the bloc to immediately perform the list fetch:
ExampleBloc _exampleBloc;
#override
void initState() {
super.initState();
_exampleBloc = ExampleBloc()..add(FetchNewList());
}
The layout, a grid view holding buttons with the string param as label follow by the int value, it's build by a BlocConsumer once the list is fetched:
BlocConsumer(
cubit: _exampleBloc,
builder: (context, state) {
if (state is ListLoadingState) {
return CircularProgressIndicator();
} else if (state is ListLoadedState) {
return GridView.count(
padding: const EdgeInsets.all(20),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: state.exampleList
.map((e) => FlatButton(
onPressed: () => _openSecondScreen(),
child: Text('${e.label} ${e.value}'),
))
.toList());
} else if (state is ListFetchErrorState) {
return FlatButton(
onPressed: () =>
_exampleBloc = ExampleBloc()..add(FetchNewList()),
child: Text('try again'));
} else {
return Container();
}
},
listener: (context, state) {
//will be used for other purpose
},
),
Non-blocking errors (for problems on list edit operations basically) are shown in a snackbar.
Clicking on a button open a second page, where the same list is loaded as a ListView and some actions allows making changes on list items (for simplicity sake a tap increment the int value of 1 while a long click delete the item from the list):
BlocConsumer(
cubit: _exampleBloc,
builder: (context, state) {
if (state is ListLoadedState) {
return ListView.builder(
itemCount: state.exampleList.length,
itemBuilder: (BuildContext context, int index) {
var item = state.exampleList[index];
return ListTile(
onTap: () => _exampleBloc.add(UpdateItemEvent(
Object(item.label, item.value + 1))),
onLongPress: () =>
_exampleBloc.add(DeleteItemEvent(item)),
title: Text(
'${item.label} + value: ${item.value.toString()}'),
);
});
} else {
return Container();
}
},
listener: (context, state) {
if (state is ListUpdateErrorState) {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage)),
);
} else if (state is ListUpdateInProgressState) {
_showCircularProgressIndicator();
}
},
));
First problem: as the list is the same for both pages, I thought it would be ok to use the same bloc but, even if the last state on the first page is the "ListLoadedState", once the second page is loaded the bloc builder is not building the corresponding widget. As workaround I thought to store the fetched list in a variable in the bloc class and adding an event (GetListAlreadyLoaded) in the initState of the second page to force the bloc to emit once again the ListLoadedState holding the list. Is there a better way to retrieve the last state of the bloc?
Second problem: if any error occurs while performing an update/delete operations on the second page, I would like to simply show a snackbar with an error message. So in the bloc class I have something like this:
#override
Stream<ExampleState> mapEventToState(ExampleEvent event) async* {
//more events...
} else if (event is PerformListUpdate) {
yield ListUpdateInProgressState();
//performing remote update
var updateResult = await _repository.performUpdate(event.objectToUpdate)
//updating local list
if (updateResult.isSuccessful) {
var index = exampleList.indexWhere(
(element) => element.label == event.objectToUpdate.label);
exampleList[index] = event.objectToUpdate;
//triggering the bloc builder with the updated list
yield ListLoadedState(exampleList);
} else {
//emit a state in order to show the error message
yield ListUpdateErrorState(
"An error occurred while performing update");
}
}
}
}
The problem is that if the ListUpdateErrorState is emitted the snackbar is shown, but the bloc builder is triggered by the new state and it rebuilds the widget in the else branch, which is an empty container. As a workaround I thought to first emit the ListUpdateErrorState to allow the listener function react and show the snackbar then, soon after, emit again the ListLoadedState with the last list value in order to trigger also the builder and show again the list view. Is that okay or there's a better way to show errors?
Third problem (basically the same as the second): while performing an asynchronous operation on the second page I would like to show some CircularProgressIndicator without "losing" the list view which could be for example in the appbar, at the bottom of the list or in the middle of the screen above the list. Emitting the "ListUpdateInProgressState" while starting the operation and reacting to it in the bloc listener however triggers the builder function which "destroys" the List view. How can I show the loading indicator without losing the list view?
I'm working on an application in which the user selects an option from a list on Screen A, then details for that item are retrieved and loaded on Screen B.
I'm confused on how to make this work. FutureBuilder requires that the Future be acquired before the build() function, like in initState(). However, routing arguments are obtained through the BuildContext with ModalRoute.of(myBuildContextHere).settings.arguments, and the BuildContext is only available to the build() function.
Moreover, it seems that Screen B is not actually disposed of when the back button is used to return to Screen A. In my very specific case Screen B displays services provided by a Bluetooth device selected in Screen A. If the selected device is not disconnected when returning to Screen A, it will not appear in the list of available devices. When the device is disconnected, Screen B runs into an error when retrieving details about the characteristics for a disconnected device, though I believe Flutter should've disposed of this Widget when it was popped from the Navigation stack. This issue stems from creating the Future for the FutureBuilder at build-time.
Screen A:
Button(
...
onPressed: () {
Navigator.pushNamed(context, '/screenB', arguments: ScreenBArguments("www.google.com"));
},
...
)
Screen B:
#override
void initState() {
// Oh if only I could access those arguments here!
// setState(() {
// future = _resolveIP(ModalRoute.of(myBuildContext).settings.arguments.url);
// });
}
#override
Widgetbuild build(BuildContext context) {
String URL = ModalRoute.of(context).settings.arguments.url;
return Scaffold(
body: Center(
child: FutureBuilder<String>(
future: _resolveIP(URL), // I can technically spawn a Future here, but now it's created at build-time.
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(ConnectState.done) return Text(snapshot.data);
}
)
)
);
}
The problem is you don't have BuildContext in insitState()
Please try this
#override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
var result = ModalRoute.of(myBuildContext).settings.arguments.url;
// setState(()=>{});
});
}
#override
Widgetbuild build(BuildContext context) {
String URL = ModalRoute.of(context).settings.arguments.url;
return Scaffold(
body: Center(
child: FutureBuilder<String>(
future: result, // I can technically spawn a Future here, but now it's created at build-time.
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(ConnectState.done) return Text(snapshot.data);
}
)
)
);
}
I am trying to run a function(with arguments) inside two-levels down StateFul widget, by clicking a button in the parent of the parent of that child(after having all widgets built, so not inside the constructor). just like in the image below:
More details is that I created a Carousal which has Cards inside, published here.
I created it with StreamBuilder in mind(this was the only use case scenario that I used it for so far), so once the stream send an update, the builder re-create the whole Carousal, so I can pass the SELECTED_CARD_ID to it.
But now I need to trigger the selection of the carousal's Cards programmatically, or in another word no need for two construction based on the snapshot's data like this:
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableCarousal(
selectedId: snapshot.data.toInt(),
onTap: //Update the stream
//some props...,
);
} else {
return SelectableCarousalLoading(
selectedId: null,
onTap: //Update the stream
//some props...,
);
}
},
);
But instead, I'm trying to have something like this so I can use it for others use cases:
Widget myCarousal = SelectableCarousal(
selectedId: null,
onTap: //Update the stream
//some props...,
);
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
// Then when data ready I can update
// the selection by calling two-level down function
if (snapshot.hasData) {
myCarousal.selectById(3);
}
// Build same carousal in all cases.
return myCarousal;
},
);
so this led me to my original question "How to run a function(with arguments) inside two-levels down StateFul widget?".
I appreciate any help. thanks a lot.
I was able to solve that challenge using the BLoC & Stream & StreamSubscription, see the image below:
Inside the Homepage screen:
///...Inside Homepage screen level-0
RaisedButton(
child: Text('Update value in the BLoC'),
onPressed: () {
bloc.changeSelectedState(isSel);
},
),
//...
inside the BLoC:
class Bloc {
final BehaviorSubject<bool> _isSelectedStreamController = BehaviorSubject<bool>();
// Retrieve data from stream
Stream<bool> get isSelectedStream => _isSelectedStreamController.stream;
// Add data to stream
Function(bool) get changeSelectedState => _isSelectedStreamController.sink.add;
void dispose() {
_isSelectedStreamController.close();
}
}
final bloc = Bloc();
Inside any widget in any level as long as it can reach the bloc:
// This inside the two-levels down stateful widget..
StreamSubscription isSelectedSubscription;
Stream isSelectedStream = bloc.isSelectedStream;
isSelectedSubscription = isSelectedStream.listen((value) {
// Set flag then setState so can show the border.
setState(() {
isSelected = value;
});
});
//...other code
#override
Widget build(BuildContext context) {
return Container(
decoration: isSelected
? BoxDecoration(
color: Colors.deepOrangeAccent,
border: Border.all(
width: 2,
color: Colors.amber,
),
)
: null,
//...other code
);
}
so the new design of my widget includes the BLoC as a main part of it, see the image:
and...works like a charm with flexible and clean code and architecture ^^
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.