Flutter: Display content from paginated API with dynamic ListView

I am new to Flutter

Welcome!

First of all, I want to express my concern against paginated APIs, since podcasts can be added while the user scrolls the list, resulting in podcasts missing or being displayed twice.

Having that out of the way, I’d like to point out that your question is quite broadly phrased, so I’ll describe my own, opinionated approach on how I would do state management in this particular use case.
Sorry for not providing sources, but Flutter and BLoC pattern are two relatively new things, and applications like paginated loading still need to be explored.

I like your choice of BLoC pattern, although I’m not sure the entire list needs to rebuild every time some new podcasts loaded.

Also, the pedantically BLoC-y way of doing things entirely with Sinks and Streams is sometimes overly complex.
Especially if there is no continual “stream of data” but rather just a single point of data transimission, Futures do the job quite well.
That’s why I’d generate a method in the BLoC that gets called every time a podcast needs to be displayed. It abstracts from the number of podcasts on a page or the concept of loading – it simply returns a Future<Podcast> every time.

For example, consider a BLoC providing this method:

final _cache = Map<int, Podcast>();
final _downloaders = Map<int, Future<List<Podcast>>>();

/// Downloads the podcast, if necessary.
Future<Podcast> getPodcast(int index) async {
  if (!_cache.containsKey(index)) {
    final page = index / 10;
    await _downloadPodcastsToCache(page);
  }
  if (!_cache.containsKey(index)) {
    // TODO: The download failed, so you should probably provide a more
    // meaningful error here.
    throw Error();
  }
  return _cache[index];
}

/// Downloads a page of podcasts to the cache or just waits if the page is
/// already being downloaded.
Future<void> _downloadPodcastsToCache(int page) async {
  if (!_downloaders.containsKey(page)) {
    _downloaders[page] = NetworkProvider().getRecentPodcasts(page);
    _downloaders[page].then((_) => _downloaders.remove(page));
  }
  final podcasts = await _downloaders[page];
  for (int i = 0; i < podcasts.length; i++) {
    _cache[10 * page + i] = podcasts[i];
  }
}

This method provides a very simple API to your widget layer. So now, let’s see how it look from the widget layer point of view. Assume, you have a PodcastView widget that displays a Podcast or a placeholder if podcast is null. Then you could easily write:

Widget build(BuildContext context) {
  return ListView.builder(
    itemBuilder: (ctx, index) {
      return FutureBuilder(
        future: PodcastsProvider.of(ctx).getPodcast(index),
        builder: (BuildContext ctx, AsyncSnapshot<Podcast> snapshot) {
          if (snapshot.hasError) {
            return Text('An error occurred while downloading this podcast.');
          }
          return PodcastView(podcast: snapshot.data);
        }
      );
    }
  );
}

Pretty simple, right?

Benefits of this solution compared to the one from your link:

  • Users don’t lose their scroll velocity if they scroll fast, the scroll view never “blocks”.
  • If users scroll fast or the network latency is high, multiple pages may be loaded simultaneously.
  • The podcast lifespan is independent of the widget lifespan. If you scroll down and up again, the podcasts aren’t reloaded although the widgets are. Because network traffic is usually a bottleneck, this will often be a tradeoff worth doing. Note that this may also be a downside, as you need to worry about cache invalidation if you got tens of thousands of podcasts.

TL;DR: What I like about that solution is that it’s inherently flexible and modular because the widgets themselves are quite “dumb” – caching, loading etc. all happens in the background.
Making use of that flexibility, with a little bit of work, you could easily achieve these features:

  • You could jump to an arbitrary id, causing only the necessary widgets to be downloaded.
  • If you want to make a pull-to-reload functionality, just throw out all the cache (_cache.clear()) and the podcasts will be re-fetched automatically.

Leave a Comment