Loading paginated data with list views in Flutter

It is common for applications to retrieve a collection of data and render the results in the form of a list. In Flutter, this is achieved using the ListView widget...

It is common for applications to retrieve a collection of data and render the results in the form of a list. In Flutter, this is achieved using the ListView widget. However, some APIs may deal with large volumes of data that when a request to retrieve that data is made, only a small subset will be returned. Typically, the response will including information to retrieve the next subset i.e. the application is dealing with paginated data. For applications that display the data in a list view, a common approach is to initiate a request to load the next set of data once the user scrolls to the bottom e.g. once the last item scrolls into view.

To do this in Flutter, we can make use of the ListView.builder constructor, which exposes an itemBuilder callback that will pass the item's index to your application so that it can render the appropriate UI for that item. For developers with experience doing Windows or Xamarin.Forms application development, this would be similar to the item/data template selector. This callback is triggered when an item's corresponding widget is visible in the list view. We could use this to trigger a request to load more items when the last item scrolls into view. Using this approach allows us to avoid doing things like calculating the scroll position, the height of the items etc to determine if the application should load the next set of data items. The resulting code would look similar to the following

/// In your state class
Future _loadMoreItems() async {
  setState(() {
    _loadingMore = true;
  });
  // make asynchronous API request to load more items here
  ...
  setState(() {
    // update _hasMoreItems if there are more
    _hasMoreItems = ...;
    _loadingMore = false;
  });
}

/// Within your state class' build function

return ListView.builder(itemCount: items.length,
                        itemBuilder: (context, index) {
                        final item = items[index];
                        // check if the last item in the list view has scrolled into view and if there are more items
                        if ((_hasMoreItems ?? false) &&
                            index == items.length - 1) {
                          if (!(_loadingMore ?? false)) {
                            // load more items
                            _loadMoreItems();
                          }
                          // display the item and possibly an indicator of some sort to show that the app is loading more items
                          ...
                        }
                        // return a widget that renders how the item looks
                        return new ItemCard(..);
});

This is pretty straightforward and the Gist above has been modified for brevity. This is an example of how it could look in action

An example of an incrementally loading list view

Upon scrolling to the last item within the current collection, it will render the item and an additional placeholder item to indicate that there are more items being loaded. This is a bit similar to what you'd see in Facebook when scrolling through your feed. A working example application from which the gif was captured from can be found here on GitHub.

Although there's not much code needed to implement the behaviour, if you find it a bit tedious to write this code for each project, I've also created a package that uses the same logic as above and can be obtained from Pub. It's essentially an extension the ListView.builder constructor with additional properties for determining if there are more items to be loaded, the function to be invoked to when there are more items and the ability to customise when more items should be loaded i.e. rather than triggering a request to load more items when the last item scrolls into view, this can be done when the 3rd last item scrolls into view.

Update: Have found that this approach doesn't work on the current version in the dev channel (0.5.7). However, I've kept this post for historical purposes. This appears to be due to the logic trying to trying to re-build whilst it's already in progress. This has resulted in the pub package needing updates to resolve issue such that the approach used now differs than what was originally proposed above. Essentially a StreamBuilder now wraps around a ListView.builder. Upon scrolling to, say, the last item, we can emit an item onto a stream that would trigger an async generator function that calls our code to load more items and emit an event when this is done. The StreamBuilder will respond to this to rebuild the ListView to show the new items. If you are interested in the code. Feel free to look at it on GitHub here