Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Infinite loop on using FutureBuilder with API call

Tags:

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

like image 588
Mandel Avatar asked Jun 03 '19 09:06

Mandel


Video Answer


2 Answers

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);
              }),
        ),
like image 54
Gunhan Avatar answered Nov 07 '22 00:11

Gunhan


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.

like image 40
enad germani Avatar answered Nov 07 '22 01:11

enad germani