How to wait for async API calls in loop - flutter

I have a widget where on initState, I need to loop over some controls and load data via an API.
I want to use/reference this data in the widget control key.
This is summary of code initializing data.
List<dynamic> controlData = [];
#override
initState(){
loadAllOptionData();
super.initState();
}
void loadAllOptionData() async {
List<dynamic> listDataControls = [];
Future.wait(widget.fields.where((element) => element['header'] !=null).map((e) async {
if(e['uitype']==7){
List<dynamic> options = await ApiManager.getListData(widget.tableId, e);
listDataControls.add({e['data'] : options});
}
}).toList());
this.setState(() {
controlData= listDataControls;
});
}
In the widget render tree, I have this check
if(controlData.length>0){
return Container(
margin: EdgeInsets.all(20),
child: Text('has data')
);
}
Right now, it's not working, as it never shows "has data". How do I get this data loaded, such that I can reference in widget tree?

Related

My future method is not working fine, while using flutter builder widget, Where did I go wrong?

Here is my stateful widget and url is a property pass it to the widget from parent widget. I don't know where did I go wrong?? I created a future builder widget that has getData() as a future. But the print statement inside was not executed ever. Why is that and it returns me always null value, and this results me a red container appearing on screen and not the table widget.
class TimeTable extends StatefulWidget {
final url;
const TimeTable({Key? key,required this.url}) : super(key: key);
#override
_TimeTableState createState() => _TimeTableState();
}
class _TimeTableState extends State<TimeTable> {
Future<List<Train>> getData() async{
final list = await TrainClient(url: widget.url).getName();
print("this line not executed");
return list;
}
#override
Widget build(BuildContext context) {
return Scaffold(body: FutureBuilder(
future: getData(),
builder: (context,projectSnap){
if(projectSnap.connectionState == ConnectionState.none ||
projectSnap.data == null) {
return Container(color: Colors.red,);
}
return buildDataTable(trains: projectSnap.data);
}));
}
}
getData is a future method and it returns a list, The list gets printed when I call that object Train Client. I had my print statement inside TrainClient class to check whether the list is created successfully.
Here is the code of TrainClient
class TrainClient {
final String url;
TrainClient({required this.url});
Future<List<Train>> getName() async {
final uri = Uri.parse(url);
final response = await get(uri);
if (response.statusCode == 200) {
print("ulla");
final data = json.decode(response.body);
final result = data["RESULTS"]["directTrains"]["trainsList"];
final list = result.map((json) => Train.fromJson(json));
print(list);
return list;
}else{
throw Exception();
}
}
}
The TrainClient class has no error since it printed the list successfully as shown below
(Instance of 'Train', Instance of 'Train', Instance of 'Train', ..., Instance of 'Train', Instance of 'Train')
You should always obtain future earlier (in initState/didChangeDependencies).
Each time your build is executed, new future is created. So it never finishes, if your widget rebuilds often.
late final _dataFuture = getData();
...
FutureBuilder(
future: _dataFuture,
builder: (context,projectSnap){
...
}
);

Flutter - Providers and Future calls, how to share the same instance?

I'm learning Flutter and there is something I cannot grasp my head around.
I implemented a Infinite scroll pagination, with a package (infine_scroll_pagination),
it works fine, but the data this Package is getting, comes from a Future call, which takes data from the WEB, and parses it in my Provider Class.
My issue is, the data that is loaded by the Infinite Scroll widget, cannot be accessed, in its state, anywhere else.
Example:
Let's take a contact list, that loads 10 contacts at a time:
class ContactsBody extends StatefulWidget {
#override
_ContactsBodyState createState() => _ContactsBodyState();
}
class _ContactsBodyState extends State<ContactsBody> {
static const _pageSize = 10;
final PagingController<int, Contact> pagingController =
PagingController(firstPageKey: 0);
#override
void initState() {
super.initState();
pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
Future<void> _fetchPage(int pageKey) async {
try {
final newItems = await ContactsService().fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
pagingController.appendLastPage(newItems.contacts);
} else {
final nextPageKey = pageKey + 1;
pagingController.appendPage(newItems.contacts, nextPageKey);
}
} catch (error) {
pagingController.error = error;
}
}
#override
Widget build(BuildContext context) {
return ContactsList(pagingController);
}
#override
void dispose() {
pagingController.dispose();
super.dispose();
}
So basically this Infinite Scroll package, will fetch my contacts, 10 at a time, and here my ContactsService call:
Future<Contacts> fetchContactsPaged(int pageKey, int pageSize) async {
final response = await http.get(.....);
if (response.statusCode == 200) {
return Contacts.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load contacts');
}
}
And finally, as you can see here above, it initializes my Provider class (Contacts), using its factory method, "fromJson()", and returns the parsed data.
Now my Provider class:
class Contacts extends ChangeNotifier {
List<Contact> _contacts = <Contact>[];
Contacts();
factory Contacts.fromJson(final Map<String, dynamic> json) {
final Contacts contacts = Contacts();
if (json['data'] != null) {
json['data'].forEach((contact) {
contacts.add(Contact.fromJson(contact));
});
}
return contacts;
}
void add(final Contact contact) {
this._contacts.add(contact);
this.notifyListeners();
}
The problem I'm having here is, when the Inifinite Scroll listView is loaded, and for example I change the state of a single contact (contacts can be set as favorite for example),
How can I access the SAME instance of the Contacts() class, that the FUTURE call initialized, so that I can access the current state of the data in that class?
Of course if I were to POST my changes onto the API, and refetch the new values where I need them, I would get the updated state of my data, but I want to understand how to access the same instance here and make the current data available inside the app everywhere
EDIT : I removed the original answer to give a better sample of what the OP wants to achieve.
I made a repo on GitHub to try to show you what you want to achieve: https://github.com/Kobatsu/stackoverflow_66578191
There are a few confusing things in your code :
When to create instances of your objects (ContactsService, Contacts)
Provider usage
(Accessing the list of the pagingController ?)
Parsing a JSON / using a factory method
The repository results in the following :
When you update the list (by scrolling down), the yellow container is updated with the number of contacts and the number of favorites.
If you click on a Contact, it becomes a favorite and the yellow container is also updated.
I commented the repository to explain you each part.
Note: the Contacts class in your code became ContactProvider in mine.
The ContactsService class to make the API call :
class ContactsService {
static Future<List<Contact>> fetchContactsPaged(
int pageKey, int pageSize) async {
// Here, you should get your data from your API
// final response = await http.get(.....);
// if (response.statusCode == 200) {
// return Contacts.fromJson(jsonDecode(response.body));
// } else {
// throw Exception('Failed to load contacts');
// }
// I didn't do the backend part, so here is an example
// with what I understand you get from your API:
var responseBody =
"{\"data\":[{\"name\":\"John\", \"isFavorite\":false},{\"name\":\"Rose\", \"isFavorite\":false}]}";
Map<String, dynamic> decoded = json.decode(responseBody);
List<dynamic> contactsDynamic = decoded["data"];
List<Contact> listOfContacts =
contactsDynamic.map((c) => Contact.fromJson(c)).toList();
// you can return listOfContacts, for this example, I will add
// more Contacts for the Pagination plugin since my json only has 2 contacts
for (int i = pageKey + listOfContacts.length; i < pageKey + pageSize; i++) {
listOfContacts.add(Contact(name: "Name $i"));
}
return listOfContacts;
}
}
Usage of Provider :
Consumer<ContactProvider>(
builder: (_, foo, __) => Container(
child: Text(
"${foo.contacts.length} contacts - ${foo.contacts.where((c) => c.isFavorite).length} favorites"),
padding: EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
color: Colors.amber,
)),
Expanded(child: ContactsBody())
]),
)
Fetch page method in the ContactsBody class, where we add the contact to our ContactProvider :
Future<void> _fetchPage(int pageKey) async {
try {
// Note : no need to make a ContactsService, this can be a static method if you only need what's done in the fetchContactsPaged method
final newItems =
await ContactsService.fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + newItems.length;
_pagingController.appendPage(newItems, nextPageKey);
}
// Important : we add the contacts to our provider so we can get
// them in other parts of our app
context.read<ContactProvider>().addContacts(newItems);
} catch (error) {
print(error);
_pagingController.error = error;
}
}
ContactItem widget, in which we update the favorite statuts and notify the listeners :
class ContactItem extends StatefulWidget {
final Contact contact;
ContactItem({this.contact});
#override
_ContactItemState createState() => _ContactItemState();
}
class _ContactItemState extends State<ContactItem> {
#override
Widget build(BuildContext context) {
return InkWell(
child: Padding(child: Row(children: [
Expanded(child: Text(widget.contact.name)),
if (widget.contact.isFavorite) Icon(Icons.favorite)
]), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10),),
onTap: () {
// the below code updates the item
// BUT others parts of our app won't get updated because
// we are not notifying the listeners of our ContactProvider !
setState(() {
widget.contact.isFavorite = !widget.contact.isFavorite;
});
// To update other parts, we need to use the provider
context.read<ContactProvider>().notifyContactUpdated(widget.contact);
});
}
}
And the ContactProvider :
class ContactProvider extends ChangeNotifier {
final List<Contact> _contacts = [];
List<Contact> get contacts => _contacts;
void addContacts(List<Contact> newContacts) {
_contacts.addAll(newContacts);
notifyListeners();
}
void notifyContactUpdated(Contact contact) {
// You might want to update the contact in your database,
// send it to your backend, etc...
// Here we don't have these so we just notify our listeners :
notifyListeners();
}
}

Why this shake functionality not working in flutter?

This ShakePlugin is not working with this piece of code,when im just using this code without these api calls and all its working fine.
class MyAppState extends State<MyApp> {
List data;
String _search = 'nature';
int index = 0;
File imageFile;
String imageData;
bool dataLoaded;
var path;
int count = 10;
FlutterShakePlugin _shakePlugin;
void initState() {
super.initState();
_shakePlugin = FlutterShakePlugin(
onPhoneShaken: () {
setState(() {
count=count+10;
});
},
},
)..startListening();
}
void dispose() {
super.dispose();
_shakePlugin.stopListening();
}
Future<String> getjsondata() async {
try {
var response = await http.get(
'https://api.unsplash.com/search/photos?per_page=${count}&client_id=TcAQEO3JoMG90U7Rl-YUiDo1x9XbZukzMOMQhxUVCV4&query=${_search}');
setState(() {
var converted = json.decode(response.body);
data = converted['results'];
});
} catch (e) {}
return 'success';
}
void saveImage(int i) async {
var url = data[i]['urls']['small'].toString();
var imageId = await ImageDownloader.downloadImage(url);
path = await ImageDownloader.findPath(imageId);
}
#override
Widget build(BuildContext context) {
getjsondata();
return GestureDetector(
child: SwipeDetector(
child: Container(
child: Image.network(
data[index]['urls']['small'],
I want to increase the count of images i recieve from api on shake of screen but this is not working even if i have installed all the libraries and all.
Calling your getjsondata method in the build method will cause the ui to render infinitely because you're calling setState in getjsondata. I think the shake plugin is working fine but its result is void because the screen is in an infinite render state.
If you move getjsondata to a FutureBuilder, remove the setState call from inside the getjsondata method and render your ui on the result of the Future your code should work.

Future<List<dynamic>> cannot be assigned to List<dynamic>

Getting the error (title) when I try to assign the return of this method to a widget parameter. The suggestions: is expect a List<dynamic> not a Future<List<dynamic>>. Is FutureBuilder the only way? The AutoCompleteTextField in this widget is a type ahead so will be calling getLocationSuggestionsList every .5 seconds after keystrokes stop (not sure if that matters in answering this question).
#override
Widget build(BuildContext context) {
return Container(
child: new Center(
child: Column(children: <Widget>[
new Column(children: <Widget>[
searchTextField = AutoCompleteTextField<dynamic>(
suggestions: getLocationSuggestionsList("sd"),
style: new TextStyle(color: Colors.black, fontSize: 16.0),
decoration: new InputDecoration(
.....
Future<List<dynamic>> getLocationSuggestionsList(String locationText) async {
List<String> suggestionList = List();
Map suggestionsKeyValuePairs = Map<String, String>();
dynamic data = await GoogleMapsServices.getAddressPrediction(
locationText,
LatLng(currentLocation.latitude, currentLocation.longitude),
);
if (data != null) {
for (dynamic predictions in data.predictions) {
suggestionsKeyValuePairs[predictions.description] = predictions.placeId;
if (!suggestionList.contains(predictions.description))
suggestionList.add(predictions.description);
}
return suggestionList;
} else {
return [''];
}
}
The cause for this error is that the suggestions parameter expects a List not a Future.
What you can do is create a state variable and assign the result of your getLocationSuggestionsList() function to that with a setState() call or any other state management mechanism so that whenever the state changes the UI builds again with the relevant data.
class YourClass extends StatefulWidget {
///
}
class _YourClassState extends State<YourClass>{
/// Your state variable here. Initialize with data that will be showing if actual data not available.
List<dynamic> suggestionList = ["];
initState(){
/// call you get suggestion function on init or any other lifecycle methods as per your need, may be inside build
getLocationSuggestionsList("sd");
super.initState();
}
#override
Widget build(context){
return AutoCompleteTextField<dynamic>(
suggestions: suggestionList,
style: new TextStyle(color: Colors.black, fontSize: 16.0),
decoration: new InputDecoration()
/// ....
);
}
void getLocationSuggestionsList(String locationText) async {
List<String> sList = List();
Map suggestionsKeyValuePairs = Map<String, String>();
dynamic data = await GoogleMapsServices.getAddressPrediction(
locationText,
LatLng(currentLocation.latitude, currentLocation.longitude),
);
if (data != null) {
for (dynamic predictions in data.predictions) {
suggestionsKeyValuePairs[predictions.description] = predictions.placeId;
if (!sList.contains(predictions.description))
sList.add(predictions.description);
}
} else {
sList = [''];
}
setState((){
suggestionList = List;
/// This will render your UI again with updates suggestionList
});
}
}
getLocationSuggestionsList() is async and return a future, if you want to get the result (List<dynamic>), you need to call it with await keyword.
await getLocationSuggestionsList("sd")
But, this is only possible into async functions/methods.
You can resolve this by many ways:
Use FutureBuilder
Do it with reactive programing architecture (Bloc, Rx, raw streams, etc...)
Do it like krishnakumarcn ;) https://stackoverflow.com/a/62187158/13569191

Text widget displaying in Reload but not in Hot Reload

I have class which will fetch the data from an API and store the result in a LIST and display the content in my text widget. When I hot reload it's displaying 'Default'. But is working fine when just reload again though I guard the text widget against the NULL.
The following is my code:
class Sample extends StatefulWidget {
#override
SampleState createState() => SampleState();
}
class SampleState extends State<Sample> {
var selected = [];
#override
void initState() {
super.initState();
callAsyncFetch();
}
callAsyncFetch() async {
var url = 'http://10.0.2.2/abc.php';
var response = await http.get(url);
var jsonData = json.decode(response.body);
print(jsonData);
for (var u in jsonData) {
if (u.substring(0, 2) == 'ABC') {
selected.add(u);
}
}
for (var u in selected) {
print(u);
}
print(selected.length); // working fine
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(children: [
selected.isEmpty
? CircularProgressIndicator()
: new Text(selected[0].toString()),
// i have tried this also but no luck
new Text(selectedStudentsIT.length!=0?selectedStudentsIT[0].toString():'Default Value'),
]),
),
);
}
}
There is simple issue is that, in hot reload it will not run initState again. you can check it by printing something in initState, while in full reload it will call initState.
i think you want to display data when you get response from api.
i think this can be easily achieved by calling setState at the end of callAsyncFetch function, so when you get data from server then it will update ui.