Infinite loop on using FutureBuilder with API call - flutter

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.

Related

Is there any easy way to use a Future (which performs an http connection) inside a stateful widget without having it reconnect on every screen build?

Every time the screen is rebuilt the getJSONfromTheSite seems to get invoked. Is seems because the future is placed inside the Widget build that every time I rebuild the screen it's just calling the apiResponse.getJSONfromTheSite('sitelist') future. But When I try to simply move the apiResponse.getJSONfromTheSite('sitelist') call outside the Widget and into the initState it doesn't work at all.
I'm not fully grasping the interplay of Futures in relation to a stateful widget, but in this case I need to keep the widget stateful because Im using a pull to refresh function to rebuild my state
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder(
future: _apiResponse.getJSONfromTheSite('sitelist'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
// (Do some UI stuff)
class RemoteDataSource {
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
return Result<AppData>.success(AppData.fromRawJson(response.body));
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
A FutureBuilder, as the name suggests, wants to build you something using a FUTURE value that you provide. For that to happen, you should perform an operation outside the build method (for example, in the State class or in the initState function) and store its Future value (like a promise in javascript), to be used later on the FutureBuilder.
You have access to this value inside the FutureBuilder on the snapshot.data variable, as I can see you already know by looking at your code. The way I coded the following solution, you should no longer have issues about multiple requests to the website each time it builds the widget UI (getJSONfromTheSite will only be called once and the result from this call will be available to you inside the FutureBuilder!)
The solution:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource(); // I left this here because I'm not sure if you use this value anywhere else (if you don't, simply delete this line)
// when creating the widget's state, perform the call to the site once and store the Future in a variable
Future<Result> _apiResponseState = RemoteDataSource().getJSONfromTheSite('sitelist');
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder<SuccessState>(
future: _apiResponseState,
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
EDIT: Edited answer to use Result as the inner type of the Future (instead of SuccessState).
The FutureBuilder's behavior can be expected as following according to the documentation
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies.
It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.
If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
As stated above, if the future is created at the same time as the FutureBuilder, the FutureBuilder will rebuilt every time there's change from the parent. To avoid this change, as well as making the call from initState, one easy way is to use another Widget call StreamBuilder.
An example from your code:
class RemoteDataSource {
final controller = StreamController<AppData>();
void _apiResponse.getJSONfromTheSite('sitelist') {
// ... other lines
if (response.statusCode == 200) {
// Add the parsed data to the Stream
controller.add(AppData.fromRawJson(response.body));
}
// ... other lines
}
In your SiteListScreen:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
_apiResponse.getJSONfromTheSite('sitelist');
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<AppData>(
stream: _apiResponse.controller.stream, // Listen to the Stream using StreamBuilder
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
AppData sitelistCollection = snapshot.data;
}
},
),
);
}
This StreamBuilder is a popular concept through out most of Flutter's apps nowadays (and is the basis of many Flutter's architecture), so it's a good idea to take a good look and use the best of it.
There is a simple way you do not need to change too much coding. Like
class RemoteDataSource {
Result _result;
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
if (_result != null) {
return _result;
}
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
_result = Result<AppData>.success(AppData.fromRawJson(response.body));
return _result;
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
I only store the success result to _result, I do not sure that you want store the error result. When you rebuild the widget, it will check if it already get the success result. If true, return the stored result, it not, call api.

Flutter error : The argument type 'List<Future<Widget>>' can't be assigned to the parameter type 'List<Widget>'

I'm trying to do a list of item from Firebase Firestore (this is done) and to get for each item a different image URL from Firebase Cloud Storage.
I use a function called getPhotoUrl to change the value of the variable photoUrl. The problem is that the return is executed before getPhotoUrl. If I add await in front of the function getPhotoUrl and async after _docs.map((document), I got an error saying that The argument type 'List<Future>' can't be assigned to the parameter type 'List'.
My code:
class PhotosList extends StatefulWidget {
#override
_PhotosListState createState() => _PhotosListState();
}
class _PhotosListState extends State<PhotosList> {
String photoUrl = 'lib/assets/default-image.png';
List<DocumentSnapshot> _docs;
getPhotoUrl(documentID) {
Reference ref = storage
.ref('Users')
.child(currentUser.uid)
.child('Photos')
.child(documentID)
.child('image_1.jpg');
ref.getDownloadURL().then((value) {
setState(() {
photoUrl = value.toString();
});
}).catchError((e) {
setState(() {
print(e.error);
});
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: firestore
.collection('Users')
.doc(currentUser.uid)
.collection('Photos')
.orderBy('date')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
_docs = snapshot.data.docs;
if (_docs.isEmpty)
return Center(
child: Text("The list is empty."));
return Container(
child: ResponsiveGridList(
desiredItemWidth: 100,
squareCells: true,
minSpacing: 5,
children: _docs.map((document) {
getPhotoUrl(document.id);
return PhotosListItem(photoUrl: photoUrl);
}).toList(),
),
);
},
);
}
}
I think you mix 2 different ways. In every build cicle you map your docs and request that photoUrl, but inside that method you call setState, which re-triggers your build method. That way you should end in infinite loop of getting photo url and building your widget.
You have three options:
Load your photoUrls and store them inside your widget -> call set state -> check inside your mapping function if your photo is loaded, if yes, take it, if no, call your getPhotoUrl function
Load your photoUrls synchronously and return url from your function and set it to your PhotosListItem
(I would prefer this) Add your documentId to your photosListItem in your mapping function and inside your item you load this photo url. In this PhotoListItem you have a variable with your imageUrl and in initState you call your getPhotoUrl function
Inside your PhotoItem:
String imageUrl;
#override
void initState() {
Future.delayed(Duration.zero, () {
setState(() {
// load your data and set it to your variable
imageUrl = ..
});
});
super.initState();
}
You might use a FutureBuilder because StreamBuilder seems to be synchronous :
How to convert Future<List> to List in flutter?
Thanks for your answers guys, actually I found an other solution which is to get and write the URL in Firestore directly after uploading the image on the Storage.
This is the article which helped me a lot : https://medium.com/swlh/uploading-images-to-cloud-storage-using-flutter-130ac41741b2
(PS: some of the Firebase names changed since this article but it's still helpful.)
Regards.

How to load list only after FutureBuilder gets snapshot.data?

I have a JSON method that returns a List after it is completed,
Future<List<Feed>> getData() async {
List<Feed> list;
String link =
"https://example.com/json";
var res = await http.get(link);
if (res.statusCode == 200) {
var data = json.decode(res.body);
var rest = data["feed"] as List;
list = rest.map<Feed>((json) => Feed.fromJson(json)).toList();
}
return list;
}
I then call this, in my initState() which contains a list hello, that will filter out the JSON list, but it shows me a null list on the screen first and after a few seconds it loads the list.
getData().then((usersFromServer) {
setState(() {. //Rebuild once it fetches the data
hello = usersFromServer
.where((u) => (u.category.userJSON
.toLowerCase()
.contains('hello'.toLowerCase())))
.toList();
users = usersFromServer;
filteredUsers = users;
});
});
This is my FutureBuilder that is called in build() method, however if I supply the hello list before the return statement, it shows me that the method where() was called on null (the list method that I am using to filter out hello() )
FutureBuilder(
future: getData(),
builder: (context, snapshot) {
return snapshot.data != null ?
Stack(
children: <Widget>[
CustomScrollView(slivers: <Widget>[
SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
child: puzzle[0],
);
},
childCount: 1,
),
)
]),
],
)
:
CircularProgressIndicator();
});
You are calling your getData method multiple times. Don't do that. Your UI waits for one call and your code for the other. That's a mess. Call it once and wait for that call.
You need to define the future to wait for in your state:
Future<void> dataRetrievalAndFiltering;
Then in your initstate, assign the whole operation to this future:
(note that I removed the setState completely, it's not needed here anymore)
dataRetrievalAndFiltering = getData().then((usersFromServer) {
hello = usersFromServer.where((u) => (u.category.userJSON.toLowerCase().contains('hello'.toLowerCase()))).toList();
users = usersFromServer;
filteredUsers = users;
});
Now your FurtureBuilder can actually wait for that specific future, not for a new Future you generate by calling your method again:
FutureBuilder(
future: dataRetrievalAndFiltering,
builder: (context, snapshot) {
Now you have one Future, that you can wait for.
This response is a little too late to help you, but for anyone wondering how to load a Future and use any of it's data to set other variables without having the issue of saving it in a variable, and then calling setState() again and loading your future again, you can, as #nvoigt said, set a variable of the Future in your state, then you can call the .then() function inside the addPostFrameCallback() function to save the result, and finally using the future variable in your FutureBuilder like this.
class _YourWidgetState extends State<YourWidget> {
...
late MyFutureObject yourVariable;
late Future<MyFutureObject> _yourFuture;
#override
void initState() {
super.initState();
_yourFuture = Database().yourFuture();
WidgetsBinding.instance?.addPostFrameCallback((_) {
_yourFuture.then((result) {
yourVariable = result;
// Execute anything else you need to with your variable data.
});
});
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _yourFuture,
builder: ...
);
}
}
dynamic doSomeStuffWithMyVariable() {
// Do some stuff with `yourVariable`
}
So, the advantage of this is that you can use the data loaded in the future outside the scope of the FutureBuilder, and only loading it once.
In my case, I needed to get a map of objects from my future, then the user could select some of those objects, save them on a map and make some calculations based on that selection. But as selecting that data called setState() in order to update the UI, I wasn't able to do so without having to write a complicated logic to save the user selection properly in the map, and having to call the future again outside the scope of the FutureBuilder to get it's data for my other calculations.
With the example above, you can load your future only once, and use the snapshot data outside the scope of the FutureBuilder without having to call the future again.
I hope I was clear enough with this example. If you have any doubts I will gladly clarify them.

State.initState() must be a void method without an `async` keyword

![State.initState() must be a void method without an async keyword.
how can i solve this probelms]1
#override
Future<void> initState() async {
// TODO: implement initState
super.initState();
_current_location();
BitmapDescriptor.fromAssetImage(
ImageConfiguration(devicePixelRatio: 2.5),
'assets/fff.png').then((onValue) {
pinLocationIcon = onValue;
});
//createCustomMarker(context);
// final Marker marker = Marker(icon: BitmapDescriptor.fromBytes(markerIcon));
DatabaseReference ref = FirebaseDatabase.instance.reference();
ref.child('users').once().then((DataSnapshot snapshot) {
Map<dynamic, dynamic> values = snapshot.value;
print(values.toString());
values.forEach((k, v) {
allMarkers.add(Marker(
markerId: MarkerId(k),
draggable: false,
icon: pinLocationIcon,
position: LatLng(v["latitude"], v["longitude"]),
infoWindow: InfoWindow(title: v["name"]),
onTap: () {
_onMarkerTapped(v["name"]);
},
),);
});
});
}
initState must be a method which takes no parameters and returns void. This is because it overrides the method of the same name in the superclass (either StatelessWidget or State<StatefulWidgetType>. As such, this limitation is a contract that is fixed and binding; you cannot change it.
Of course, this also means that initState cannot be marked as async. This is because any method marked as async will implicitly return a Future, but if the method returns anything, it cannot have a return type of void which breaks the override contract.
If you need to call an async method from within initState, you can do so simply by not awaiting it:
#override
void initState() {
super.initState();
doSomeAsyncStuff();
}
Future<void> doSomeAsyncStuff() async {
...
}
If, however, you need the data from the async method for your widget, you cannot simply wait for the Future to return before you build the widget. Flutter does not allow this, because there is no telling how long it will take for the Future to return, and stalling the widget building until then could potentially block your entire app.
Instead, you need to have your widget build normally and then have a way to notify your widget to update when the Future has returned. This is most easily done with a FutureBuilder:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: doSomeAsyncStuff(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
// Future hasn't finished yet, return a placeholder
return Text('Loading');
}
return Text('Loading Complete: ${snapshot.data}');
}
);
}
(Notice how instead of calling the async method from initState, I am calling it from the FutureBuilder during the build process.)
EDIT: As pointed out, this approach only works in OP's situation where the awaited future will always eventually return a value. This is not always the case - sometimes the future doesn't return a value at all and is just a long-running process. Sometimes the future might return null instead of concrete data. And sometimes the future may result in an error instead of completing successfully. In any of these cases, snapshot.data will be null after the future completes, in which case snapshot.hasData will always be false.
In these situations, instead of depending on snapshot.hasData to wait for data to appear, you can use snapshot.connectionState to monitor the state of the future itself:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: doSomeAsyncStuff(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
// Future hasn't finished yet, return a placeholder
return Text('Loading');
}
return Text('Loading Complete');
}
);
}

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.