ListView Infinite Scrolling in Flutter

4 min read, 31 August, 2018

In the article Introduction to ListView in Flutter, we have already seen the basic usage patterns of ListView in Flutter. In real-world apps, data of ListViews usually comes from a remote server, and we can easily use a FutureBuilder to create a ListView after the data is loaded. In this article, we'll implement the typical infinite scrolling pattern with ListView.

You may have seen infinite scrolling pattern when using other apps. This pattern allows users to keep scrolling down the list to see more items. New items are loaded when the user almost scrolls to the bottom of the list. This pattern gives user a smoothing reading experiences without having to click a button to see more items.

Even the pattern name suggests infinite, the number of items is still limited, just a relative large number. ListView.builder already supports creating items dynamically. We only need to set the total number of items as the value of parameter itemCount, ListView will invoke the itemBuilder function when more items need to be created. When building the widget for each item, since the data needs to be loaded from the remote server, we need to use FutureBuilder for each item. Usually, data for each item is not retrieved one by one, but in batch with pagination support. Here we use Completer to provide the Futures used by each item, and coordinate with Futures to load items.

We start from the model class Item. It's very simple with two properties: id and name.

class Item {
  int id;
  String name;

  Item({this.id, this.name});
}

We also have the function loadItems to load items. Here we use Future.delayed to simulate the asynchronous requests. offset is the start index of returned items, while limit is the total number of returned items.

Future<List<Item>> _loadItems(int offset, int limit) {
  var random = new Random();
  return Future.delayed(new Duration(seconds: 2 + random.nextInt(3)), () {
    return List.generate(limit, (index) {
      var id = offset + index;
      return new Item(id: id, name: "Item $id");
    });
  });
}

Now we move the most complicated part to implement infinite scrolling. In the code below, total is the total number of items, pageSize is the number of items to load in each request. completers is the list of Completer<Item>, which maps to items to render in ListView. In the function _loadItem, if the itemIndex is larger than or equals to the length of completers, it means we need to load more items. We calculate the number of items to load, then create a list of Completers to represent the items to load and add them to completers. Then we call _loadItems to load these items. In the then callback of the Future returned by _loadItems, we complete all Completers corresponding to these items with the Item objects. In the catchError callback, we complete all Completers with the same error.

For each item, we get the Future object from the corresponding Completer and create a FutureBuilder as the widget to render. The FutureBuilder renders a Placeholder when the item is loading, and uses the function _generateItem to render the item once it's loaded.

var total = 105;
var pageSize = 20;

var completers = new List<Completer<Item>>();

Widget _loadItem(int itemIndex) {
  if (itemIndex >= completers.length) {
    int toLoad = min(total - itemIndex, pageSize);
    completers.addAll(List.generate(toLoad, (index) {
      return new Completer();
    }));
    _loadItems(itemIndex, toLoad).then((items) {
      items.asMap().forEach((index, item) {
        completers[itemIndex + index].complete(item);
      });
    }).catchError((error) {
      completers.sublist(itemIndex, itemIndex + toLoad).forEach((completer) {
        completer.completeError(error);
      });
    });
  }

  var future = completers[itemIndex].future;
  return new FutureBuilder(
      future: future,
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            return new Container(
              padding: const EdgeInsets.all(8.0),
              child: new Placeholder(fallbackHeight: 100.0),
            );
          case ConnectionState.done:
            if (snapshot.hasData) {
              return _generateItem(snapshot.data);
            } else if (snapshot.hasError) {
              return new Text(
                '${snapshot.error}',
                style: TextStyle(color: Colors.red),
              );
            }
            return new Text('');
          default:
            return new Text('');
        }
      });
}

The function _generateItem is the same as in the previous article.

Widget _generateItem(Item item) {
  return new Container(
    padding: const EdgeInsets.all(8.0),
    child: new Row(
      children: <Widget>[
        new Image.network(
          'http://via.placeholder.com/200x100?text=Item${item.id}',
          width: 200.0,
          height: 100.0,
        ),
        new Expanded(child: new Text(item.name))
      ],
    ),
  );
}

The InfiniteListView just uses ListView.builder with _loadItem as the itemBuilder.

class InfiniteListView extends StatelessWidget {

  
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: new Text('Infinite Scrolling')),
      body: new ListView.builder(
          itemCount: total,
          itemBuilder: (BuildContext context, int index) => _loadItem(index)),
    );
  }
}

Below is the screenshot of infinite scrolling. Items to be loaded are rendered as placeholders.

Screenshot of infinite scrolling

Source code

Source code of this article is available on GitHub.