Search code examples
flutterasynchronoussetstateflutter-futurebuilder

Flutter: Variable updated in setState, not updating inside the future function in Future Builder. I am trying to generate api url dynamically


I have searched on Stackoverflow and a lot on the internet, but haven't been able to find an answer to this. My goal: I am trying to generate an API endpoint dynamically using, radio buttons and TextField. User would click on radio button that would generate the API endpoint upto the filtering criteria. eg. http://localhost:3000/student/name/ or http://localhost:3000/student/id/. Now, after this, I want the last part of this endpoint to be filled by the value from TextField, which would pinpoint to the exact search parameter. For that, I am setting state to update the searchAPICall variable by concatenating the TextField value with the endpoint generated after selecting Radio button. Now, this variable will be passed to the future: function of the future builder and populate the list with the result from the fetch http.get function. I have been going over this for last 3 days and haven't been able to do so. Please help me. This is my first question on stackoverflow, so please bear with me. Please guide me, if I am on the wrong path or unnecessarily asking question. But I have searched a lot to find answer to this.

As you can see from the below code, I have confirmed in many ways that the string url is concatenating but somehow even after setting state, the function is not updated with the latest value of the variable. As per my understanding anything that depends on the variable, whose state is updated using setState in a stateful widget is updated automatically with the latest value of the variable. I may try to solve this with the provider package, but I need to understand what am I doing wrong. What concept am I getting wrong. Is my understanding of setState wrong? What would I do in similar case when I need to update/modify/manipulate a parameter of a function dynamically, using user input or multiple user inputs. Please help me get to the bottom of this. And please, if you can, help me solve this without using any packages. Thanks! Please find my code below. (This is intended as a desktop app, no need for emulator). (Editing after original question): Concept clarification that I need is, can a state variable(whose state I am changing be passed as argument to a function that returns a future? Moreover, what value is being passed to the Future function, the updated value or previous value. As per the below example, I have tested it a thousand times, and only the previous value(value upto selecting radio button is passed and not the value after appending the textfield). I only need to understand what is happening under the hood, so I can act accordingly and find a way to pass the updated value.

import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
// import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';

import '../../models/PatientDemo.dart';
import '../patientListWidgets/patientSearchList.dart';
import '../patientListWidgets/patientSearchListCopy.dart';
import '../main_widgets/patientListViewBuilder.dart';

class PatientSearchDialog extends StatefulWidget {
  const PatientSearchDialog({super.key});

  @override
  State<PatientSearchDialog> createState() => _PatientSearchDialogState();
}

enum SearchOption {
  firstName,
  lastName,
  id,
}

class _PatientSearchDialogState extends State<PatientSearchDialog> {
  String searchAPIcall = '';
  SearchOption? selectedRadio = SearchOption.firstName;

  final searchTextFieldController = TextEditingController();
  bool isSearching = false;

  Future<List<PatientDemo>> fetchAllPatients(url) async {
    // print("fetchAllPatients in PatientDemoScreen called.");
    // print("fetchAllPatients received this end point:" + url);
    final response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
      final List result = jsonDecode(response.body);
      // print(result);
      return PatientDemo.fromJsonList(result);
    } else {
      // throw Exception('Failed to load data');
      throw Error();
    }
  }

  @override
  void initState() {
    // TODO: implement initState
  }

  @override
  void dispose() {
    // TODO: implement dispose
    searchTextFieldController.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    switch (selectedRadio) {
      case SearchOption.firstName:
        setState(() {
          searchAPIcall = "http://localhost:3000/patients/name/";
        });

        break;
      case SearchOption.id:
        setState(() {
          searchAPIcall = "http://localhost:3000/patients/id/";
        });

        break;
      default:
        searchAPIcall = "http://localhost:3000/patients/name/";

        break;
    }

    return Dialog(
      child: Container(
        margin: EdgeInsets.symmetric(horizontal: 19.0, vertical: 9.0),
        child: Column(
          children: [
            TextField(
              controller: searchTextFieldController,
              decoration: InputDecoration(
                labelText: "FirstName / LastName / PatientNo.",
              ),
              onSubmitted: (value) {
                // print(value);
                // setState(() {
                //   searchAPIcall = searchAPIcall + value;
                //   fetchAllPatients(searchAPIcall);
                //   isSearching = true;
                // });
                // print('From onSubmitted of TextField: $searchAPIcall');
              },
              onEditingComplete: () {
                // setState(() {
                //   searchAPIcall =
                //       searchAPIcall + searchTextFieldController.text;
                //   isSearching = true;
                // });
                // print('From onEditingComplete of TextField: $searchAPIcall');
                // print(searchTextFieldController.text);
              },
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              mainAxisSize: MainAxisSize.max,
              children: [
                //radiobuttons
                radioButtonListTile("Firstname", SearchOption.firstName),
                radioButtonListTile("Lastname", SearchOption.lastName),
                radioButtonListTile("Patient No.", SearchOption.id),
              ],
            ),
            isSearching
                ? FutureBuilder(
                    // future: fetchAllPatients('http://localhost:3000/patient/name/John'), // this works, but that's because this is not dynamically generated
                    future: fetchAllPatients(
                        searchAPIcall), // unable to send the dynamically generated url to this function
                    builder:
                        (context, AsyncSnapshot<List<PatientDemo>> snapshot) {
                      print(snapshot);
                      print(snapshot.data);
                      print(snapshot.connectionState);
                      if (snapshot.hasData) {
                        //List view builder import
                        return PatientListViewBuilder(data: snapshot.data);
                      } else if (snapshot.hasError) {
                        return Text(
                          '${snapshot.error}',
                        );
                      } else if (snapshot.connectionState ==
                          ConnectionState.waiting) {
                        return const Center(
                          child: CircularProgressIndicator(),
                        );
                      }
                      return Text("Error in retreiving data");
                    },
                  )
                : Padding(
                    padding: const EdgeInsets.all(19.0),
                    child: Text(
                      "Enter data in the textfield and choose the criteria for search",
                      style: TextStyle(fontSize: 19.0, color: Colors.blue),
                    ),
                  ),
            Container(
              margin: const EdgeInsets.only(
                top: 3,
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                mainAxisSize: MainAxisSize.max,
                children: [
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        searchAPIcall =
                            searchAPIcall + searchTextFieldController.text;
                        print("From SEARCH BUTTON $searchAPIcall");

                        // fetchAllPatients(searchAPIcall); // when I do this, the dynamically generated url does get passed to this function
                        isSearching = true;
                      });
                    },
                    child: const Text(
                      "SEARCH",
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () async {
                      Logger().d("OPEN pressed");
                      //TODO: Populate the previous widget with the values received from the API call by joining radiobutton and textfield.
                      // send the API end point call to the next function which will fetch the data from the end point and populate the fields
                    },
                    child: const Text(
                      "OPEN",
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      Logger().d("CANCEL pressed");
                      Navigator.pop(context);
                    },
                    child: const Text(
                      "CANCEL",
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget radioButtonListTile(String label, SearchOption searchParam) {
    return Flexible(
      child: ListTile(
        title: Text(label),
        leading: Radio<SearchOption>(
          value: searchParam,
          groupValue: selectedRadio,
          onChanged: (SearchOption? value) {
            setState(() {
              selectedRadio = value;
            });
          },
        ),
      ),
    );
  }
}


Solution

  • Thanks to user pskink for his answer in comments section.

    SetState would not work in this case. This required StreamBuilder. StreamBuilder takes the input stream and updates it inside the widget in builder field. setState has it's use case, and StreamBuilder has its own use case.

    Most probably, the issue was that I was trying to pass the value to a future function. Theoretically, it made sense, but turns out setState has it's limitations. The future function was running before the value that was being set in setState reached the future Function. And once the future Function ran with the previous value(the incomplete value before setState could update the variable's value), the function could not be executed again. Because this is not how setState works.

    While in case of Stream Builder, it continuously feeds in the input stream to the function(doesn't matter if it's a future function or usual function), and function executes again. So, even if the future function received the old incomplete value as it's argument, the StreamBuilder made it execute immediately again with the new value that the future Function did not receive before.

    Even if my understanding is way off the actual inner working of setState/StreamBuilder/Flutter in this case. The conclusion is that in such a case, StreamBuilder is the option to go with.

    Moreover, I realized that StreamBuilder is a much strong tool to update state compared to using setState. There is no way to mark a question as resolved so I am going to put it in CAPS at the start and end of this answer to notify anyone who views this page.