1

Background

Inside the state object of my stateful widget, I have the following code.

class _PendingJobsState extends State<PendingJobs> {
  List<String> pendingJobs = [];      <------------------- I am trying to change the state of this variable.

  void updateListWithResponseData(List jobs) {
    BackendApi.call(
        endpoint: APIEndPoints.GET_PENDING_JOBS,
        data: {"email": GlobalData.userEmail},
        onSuccess: (data) {
          setState(() {           <---------------- I call set State here
            jobs = data['job_list'];
            print(pendingJobs);        <------------ State is not changed
            print(jobs);
          });
        },
        onFailed: (error) {
          print('Failed to fetch data!');
        });
  }

  @override
  void initState() {
    updateListWithResponseData(pendingJobs);    <------- This is where the function in which I call the setState is executed.
    super.initState();
  }

Details about the above code

List<String> pendingJobs = []; is the variable that I am expecting to have a state change done.

The function defined right below the above variable called updateListWithResponseData takes a parameter of type List. It is also responsible for calling another utility function called BackendApi.call().

I am calling the udateListWithResponseData() inside the initState and for the parameter of type List it takes, I am giving the pendingJobs variable that I have defined. (Since I am calling setState from within the updateListWithResponseData() function, I am expecting the the state of pendingJobs to change when updateListWithResponseData is called.)

However, the state change I am expecting in the above point is not taking place.

BackendApi.call is responsible for fetching data from a given url and it takes two callback functions for onSuccess and onFailure which are responsible for performing necessary actions depending on the data fetching is a success or not.

An important note

Removing the List jobs parameter from the updateListWithResponseData and directly referring to the pendingJobs variable is not a solution for me since I am expecting to extract the function called updateListWithResponseData to a separate dart file and call it from different widgets. So I have to have that List jobs parameter in the function.


I tried to debug this issue for some time now and could not find a solution. It would be really helpful if someone can point out why the state of pendingJobs is not changing and how to actually make it change. (Thanks.)


**Edit**

Since a lot of comments below seem to revolve around the BackendApi.call() function and since I had not included the code for that function in my original post I have edited this post to include the code for that.

import 'dart:convert';
import 'package:field_app/globalData/global_data.dart';
import 'package:http/http.dart' as http;
import 'api_endpoints.dart';

typedef ValueChanged<T> = void Function(T value);

class BackendApi {
  static void call(
      {String endpoint,
      Map<String, dynamic> data,
      ValueChanged<Map<String, dynamic>> onSuccess,
      ValueChanged<String> onFailed}) async {
    try {
      var response = await http.post(APIEndPoints.API_ROOT + endpoint,
          headers: {
            "Content-Type": "application/json",
            "Authorization": 'Bearer ' + GlobalData.authToken,
          },
          body: jsonEncode(data));

      Map<String, dynamic> apiResponse = jsonDecode(response.body);
      if (apiResponse != null) {
        if (response.statusCode == 200) {
          if (onFailed != null) {
            onSuccess(apiResponse);
          }
        } else {
          print(apiResponse['message']);
          print('code: ' + response.statusCode.toString());
          if (onFailed != null) {
            onFailed(apiResponse['message']);
          }
        }
      } else {
        print('Invalid API response format');
        print('code: ' + response.statusCode.toString());
        return null;
      }
    } catch (e) {
      print("Failed to connect with backend API");
      print(e.toString());
      return null;
    }
  }
}
gfit21x
  • 141
  • 9
  • sorry, but is `BackendApi.call` is using http request or using some sort of Future? – Md. Yeasin Sheikh Sep 23 '21 at 18:05
  • @Yeasin Sheikh yes it does. `Backend.call` is an async function that sends a `post http request` – gfit21x Sep 23 '21 at 18:07
  • can you try making `updateListWithResponseData()` async and using await. – Md. Yeasin Sheikh Sep 23 '21 at 18:09
  • In `setState` you should change the value of `pendingJobs`, not `jobs`. Btw a `FutureBuilder` is a better solution for this, see my answer [here](https://stackoverflow.com/questions/69286169/elements-from-webservice-doesnt-appear-in-drag-and-drop-list-in-flutter/69287718#69287718). – Peter Koltai Sep 23 '21 at 18:10
  • @YeasinSheikh there is a little problem with doing that because `BackendApi.call()` has a return type of `void`. So I can not `await` it even if I make the `updateListWithResponseData()` an async function. – gfit21x Sep 23 '21 at 18:15
  • Since `initState` can't be `async`, it would not make a difference anyway. – Peter Koltai Sep 23 '21 at 18:16
  • @PeterKoltai I checked your answer. But there are two problems, 1) I will be extracting that function `updateListWithResponseData()` to a separate dart class later to import that function into a few other widgets and use it there. `To do that I have to pass the 'jobs' as an argument.`. 2) I would like to use `FutureBuilder` for this too, but the code I am working on is written by someone else and they want to keep it with using `callbacks` when something is done, instead of using the `FutureBuilder` – gfit21x Sep 23 '21 at 18:19
  • Let me share and test it on your side and yes it will reflect. – Md. Yeasin Sheikh Sep 23 '21 at 18:19

3 Answers3

1

The reason behind this behaviour is that dart parameters are passed by value. (i.e. a copy of the variable is passed with the variable data)

So here you are passing a copy of the values in pendingJobs which happens to be a reference to the list.

@override
  void initState() {
    updateListWithResponseData(pendingJobs);    <------- This is where the function in which I call the setState is executed.
    super.initState();
  }

and now updateListWithResponseData has its own variable jobs that holds a copy of pendingJobs reference

Future<void> updateListWithResponseData(List jobs) async{
   await BackendApi.call(
        endpoint: APIEndPoints.GET_PENDING_JOBS,
        data: {"email": GlobalData.userEmail},
        onSuccess: (data) {
          setState(() {           <---------------- I call set State here
            pendingJobs = data['job_list'];
            print(pendingJobs);        <------------ State is not changed
            print(jobs);
          });
        },
        onFailed: (error) {
          print('Failed to fetch data!');
        });
  }

so what this jobs = data['job_list']; does is assaigning the local variable (to updateListWithResponseData) jobs value, this change will not be reflected on pendingJobs as you are only updating the copy within updateListWithResponseData.

to fix this you remove the assignment jobs = data['job_list']; and replace it with jobs.addAll(data['job_list']); this way pendingJobs value will get updated too.

  • Thank you very much. Your answer worked for me. There is one thing I would like to clarify from your answer. In your answer you mentioned `to fix this you remove the assignment jobs = data['job_list']; and replace it with jobs.addAll(data['job_list']); this way pendingJobs value will get updated too.`. Now since `updateListWithResponseData` has it's own variable called `jobs`, how come `jobs.addAll(data['job_list']);` update the actual `pendingJobs` variable we wanted instead of updating the local variable that was scoped to the function? – gfit21x Sep 24 '21 at 03:06
  • 1
    I think I figured the answer to the above comment I have typed. `jobs = data['job_list']` changes the reference to `data[job_list]` from `pendingJobs`. However when we are doing the `jobs.addAll[data['job_list']]` we do not use the `assignment operator`. Hence the reference to the `pendingJobs` is not changed and it makes changes on the `pendingJobs` – gfit21x Sep 24 '21 at 05:12
  • 1
    That is correct. That is exactly what happens, and that is the expected behaviour of "Pass by value" as the called function receives a copy of the value (in your case a reference to a list) and stores it as a local variable, and when you assign any value to that variable you lose the passed value from the caller thus losing the possiblity to modify it. – Abdulrahman Ali Sep 24 '21 at 18:33
0

Try:

Future<void> updateListWithResponseData(List jobs) async{
   await BackendApi.call(
        endpoint: APIEndPoints.GET_PENDING_JOBS,
        data: {"email": GlobalData.userEmail},
        onSuccess: (data) {
          setState(() {           <---------------- I call set State here
            pendingJobs = data['job_list'];
            print(pendingJobs);        <------------ State is not changed
            print(jobs);
          });
        },
        onFailed: (error) {
          print('Failed to fetch data!');
        });
  }

And on initState:

updateListWithResponseData(pendingJobs); 

Here is a demo widget of that workflow


class ApplicantsX extends StatefulWidget {
  const ApplicantsX({Key? key}) : super(key: key);

  @override
  State<ApplicantsX> createState() => _ApplicantsXState();
}

class _ApplicantsXState extends State<ApplicantsX> {
  int a = 0;
  Future<void> up(int v) async {
    await Future.delayed(Duration(seconds: 2), () {
      setState(() {
        a = v;
      });
    });
  }

  @override
  void initState() {
    super.initState();
    up(234);
  }

  @override
  Widget build(BuildContext context) {
    return Center(child: Text("$a"));
  }
}

Md. Yeasin Sheikh
  • 54,221
  • 7
  • 29
  • 56
  • Thank your for the answer and your effort. I ran this code and still did not update the change, Also as soon as I put the term `await `, my editor gives me a warning saying `await applied to void which is not a future` – gfit21x Sep 23 '21 at 18:26
  • Yes. It is still there after the `Futue`. I think that it is because `BackendApi.call()` does not return a `Future`. So awaiting it gives a warning I think.... – gfit21x Sep 23 '21 at 18:29
  • Can you include `BackendApi.call` and I think it should return `Future` instead of void because we're requesting on HTTP and its response will be future? – Md. Yeasin Sheikh Sep 23 '21 at 18:29
  • I tried this. But still the state does not change.... I think that making the `BackendApi.call`, `Future` is not necessary, because that api util is written in a way that rather than returning any data, we just provide a callback function to it to act upon that data. I will edit my question to include the code for `BackendApi.call` – gfit21x Sep 23 '21 at 18:36
  • can you try with `static Future call() async {}` – Md. Yeasin Sheikh Sep 23 '21 at 18:47
  • 1
    `jobs = data['job_list'];` - shouldn't it be `pendingJobs = `? – Peter Koltai Sep 23 '21 at 18:53
  • yes it will be ,, – Md. Yeasin Sheikh Sep 23 '21 at 18:55
0

First thing: if you want to do an asynchronous task which will change state when resumed on first build, always do that in a WidgetsBinding.instance.addPostFrameCallback. Second thing: you don't have to use state variable as parameter for state method.

Try this:

class _PendingJobsState extends State<PendingJobs> {
  List<String> pendingJobs = [];      <------------------- I am trying to change the state of this variable.

  void updateListWithResponseData() {
    BackendApi.call(
        endpoint: APIEndPoints.GET_PENDING_JOBS,
        data: {"email": GlobalData.userEmail},
        onSuccess: (data) {
          setState(() {           <---------------- I call set State here
            pendingJobs = data['job_list'];
            print(pendingJobs);
          });
        },
        onFailed: (error) {
          print('Failed to fetch data!');
        });
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => updateListWithResponseData()) <------- This is where the function in which I call the setState is executed.
  }
bface007
  • 67
  • 6