Dev Aggarwal
Dev Aggarwal

Reputation: 8516

Flutter ListView.builder() dynamic list - shows inifinitely repeating entries

I have a large, ~5K items list.

What I wanted to do was to write a search view for these items.

I tried the following -

   List<String> items;
   String query;

   ListView.builder(
    itemBuilder: (context, index) {
      for (int i = index; i < items.length; i++) {
        var item = items[i];
        if (item.contains(query)) {
          return ItemTile(item);
        }
      }
    }

This renders and searches for elements efficiently, but the problem is that it ends up repeating the last item in the list infinitely.

I guess that's because I wasn't providing an itemCount.


So I tried to keep track of the number of filtered items myself, inside a Stateful Widget.

   var _count = 1;

   _queryController.addListener(() {
      setState(() => _count = 1);
   });

   ListView.builder(
    itemBuilder: (context, index) {
      for (int i = index; i < items.length; i++) {
        var item = items[i];
        if (item.contains(query)) {
          setState(() => _count += 1);
          return ItemTile(item);
        }
     }
     itemCount: _count;
   }

But then I receive an error, saying - setState() or markNeedsBuild() called during build.

What would be the right way to do this, without compltely searching through items for each query?

(I was aiming for a search-as-you-type UX)

Upvotes: 0

Views: 4366

Answers (3)

Dev Aggarwal
Dev Aggarwal

Reputation: 8516

Here is a possible way to add debounce :-

var _controller = TextEditingController();
List<dynamic> _filtered;

@override
void initState() {
  _filtered = widget.items;

  _controller.addListener(() {
    var query = _controller.text;
    Future.delayed(Duration(milliseconds: 250), () {
      if (!mounted) {
        return;
      }
      if (_controller.text == query) {
        setState(() {
          _filtered = widget.items
              .where((item) => item.contains(query)).toList())
              .toList();
        });
      }
    });
  });

  super.initState();
}

...

ListView.builder(
  itemBuilder: (context, index) => ItemTile(_filtered[index]),
  itemCount: _filtered.length,
)

Upvotes: 1

Sondre
Sondre

Reputation: 1898

You could try filtering the list before you pass it to the builder.

List<String> items;
List<String> _queryResults;
String query;

_queryResults = items.where((item) => item.contains(query)).toList();

Which could then be passed to the ListView.builder without having to set state over and over. Not sure how it would be in performance compared to other options, shouldn't be a big difference as the filtered list will likely be a lot short in most cases. It's a lot cleaner than doing filtering and setting state inside the builder, so I would at least test it out.

Also you should debounce the filtering if you're going for a search-as-you-type solution, which means you wait like 500ms or some fitting duration after each input to see if the user types more before you do the filtering. Will save you a lot of unnecessary calls and make your solution perform better.

Upvotes: 1

Amit Jangid
Amit Jangid

Reputation: 2889

You are using for loop inside Listview.builder which is not required. ListView.builder will automatically loop to the number of items in your list...

Try this.

List<String> items;
String query;

ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
  var item = items[index];
  if (item.contains(query)) {
      setState(() => _count += 1);
      return ItemTile(item);
  }
}

Upvotes: 0

Related Questions