Search code examples
flutterlistviewflutter-layoutflutter-listview

ListView infinite loop when parsing data from API response


I'm trying to read data from some mock endpoint. Mock endpoint I'm invoking (HTTP GET) is here.

Essentially, the JSON structure is result > toolList[] > category > tools[]. I'd like to display these items on my page in such a way that the category name is displayed first, then items belonging to that category under it. I am trying to achieve this with ListView.builder but I somehow managed to get some sort of infinite loop and the items keep getting populated until my device freezes.

What I'm trying to achieve:

  • Category Title
    • Item 1
    • Item 2
  • Category Title 2
    • Item 1
    • Item 2
    • Itme 3

And finally the Widget:

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

  @override
  _OpticsSelector createState() => _OpticsSelector();
}

class _OpticsSelector extends State<OpticsSelectorWidget> {
  PageController pageViewController;
  final scaffoldKey = GlobalKey<ScaffoldState>();

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: StandardAppbarWidget(appBarTitle: "some title"),
      body: SizedBox(
        child: FutureBuilder<ApiCallResponse>(
          future: ConfigurationController.getOpticsTools2(),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return Center(
                child: SizedBox(
                  width: 50,
                  height: 50,
                  child: CircularProgressIndicator(
                    color: Colors.red,
                  ),
                ),
              );
            }

            final gridViewGetToolsOpticsResponse = snapshot.data;

            var toolCategories = getJsonField(
              gridViewGetToolsOpticsResponse.jsonBody,
              r'''$.result.toolList''',
            ).toList();

            return Builder(
              builder: (context) {
                return ListView.builder(itemBuilder: (context, itemIndex) {
                  final widgets = <Widget>[];

                  for (int i = 0; i < toolCategories.length; i++) {
                    var currentToolCategory = getJsonField(
                      toolCategories[i],
                      r'''$.category''',
                    );

                    widgets.add(Text(
                      currentToolCategory,
                      style: Colors.white,
                    ));

                    var toolListInCategory = getJsonField(
                      toolCategories[itemIndex],
                      r'''$.tools''',
                    );

                    for (int j = 0; j < toolListInCategory.length - 1; j++) {
                      var toolDisplayName = getJsonField(
                        toolListInCategory[j],
                        r'''$.displayName''',
                      );

                      widgets.add(Text(toolDisplayName));
                    }
                  }
                  return SingleChildScrollView(
                      child: Column(
                    children: widgets,
                  ));
                });
              },
            );
          },
        ),
      ),
    );
  }
}

I'm especially confused about the itemIndex expression. That number I thought would be the item count that I receive from my API call, but I guess I'm mixing something badly.

If it helps, here's the bit where I'm making the API call. But feel free to just grab the JSON your way (from mock response)

static Future<ApiCallResponse> getOpticsTools2() async {    
    HttpOverrides.global = new MyHttpOverrides();
    var client = http.Client();
    try {
      var response = await client.get(Uri.https('stoplight.io'
      , "mocks/ragingtortoise/test/82311857/configuration/tools/optics"));

      return createResponse(response, true);
    } finally {
      client.close();
    }
}

static ApiCallResponse createResponse(http.Response response, bool returnBody) {
    var jsonBody;
    try {
      jsonBody = returnBody ? json.decode(response.body) : null;
    } catch (_) {}

    return ApiCallResponse(jsonBody, response.statusCode);
}

And the return type, which is ApiCallResponse:

class ApiCallResponse {
  const ApiCallResponse(this.jsonBody, this.statusCode);
  final dynamic jsonBody;
  final int statusCode;
  bool get succeeded => statusCode >= 200 && statusCode < 300;
}

Finally adding the screen recording of what's happening, if it helps.

enter image description here


Solution

  • I struggled for so long but clearly, the issue was not passing in the itemCount argument into the ListView.builder() method. Also, the outer loop was invalid as now I need to use the actual itemIndex within the builder. Thanks for pointing out the itemCount all! Here's the fixed code and the solution in case anyone needs it later.

    @override
    Widget build(BuildContext context) {
    final opticsToolsMockResponse = ConfigurationController.getOpticsTools2();
    
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: StandardAppbarWidget(appBarTitle: "some title"),
      body: SizedBox(
        child: FutureBuilder<ApiCallResponse>(
          future: opticsToolsMockResponse,
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return Center(
                child: SizedBox(
                  width: 50,
                  height: 50,
                  child: CircularProgressIndicator(
                    color: Colors.red,
                  ),
                ),
              );
            }
    
            final gridViewGetToolsOpticsResponse = snapshot.data;
    
            var toolCategories = getJsonField(
              gridViewGetToolsOpticsResponse.jsonBody,
              r'''$.result.toolList''',
            ).toList();
    
            return Builder(
              builder: (context) {
                return ListView.builder(
                    itemCount: toolCategories.length,
                    itemBuilder: (context, itemIndex) {
                      final widgets = <Widget>[];
    
                      var currentToolCategory = getJsonField(
                        toolCategories[itemIndex],
                        r'''$.category''',
                      );
    
                      widgets.add(Text(
                        currentToolCategory,
                        style: Colors.white,
                      ));
    
                      var toolListInCategory = getJsonField(
                        toolCategories[itemIndex],
                        r'''$.tools''',
                      );
    
                      for (int j = 0; j < toolListInCategory.length; j++) {
                        var toolDisplayName = getJsonField(
                          toolListInCategory[j],
                          r'''$.displayName''',
                        );
    
                        widgets.add(Text(toolDisplayName));
                      }
    
                      return SingleChildScrollView(
                          child: Column(
                        children: widgets,
                      ));
                    });
              },
            );
          },
        ),
      ),
    );
    

    }

    enter image description here