Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: Display content from paginated API with dynamic ListView

Tags:

flutter

I am new to Flutter so please bear with me. I have a paginated API that means on calling example.com?loaditems.php?page=0 will load first 10 items(podcasts list) and example.com?loaditems.php?page=1 will load items from 10 to 20 and so on. I want StreamBuilder to fetch page 0 first then when the list reaches the bottom it should load page 1 and show it. To check if I have reached the last item in listview I am using ScrollController of ListView.

Now I am using StreamBuilder, ListView, InheritedWidget in bloc pattern. I am not sure if I have implemented it correctly so I am gonna paste the entire code. My question is, is this is the correct BLOC pattern way to do it? If not then what is it? I also came across with this article: https://crossingthestreams.io/loading-paginated-data-with-list-views-in-flutter/ In the end it says "Update:" but I could not understand it much.

Here' the app's entry point:

void main() => runApp(new MaterialApp(
title: "XYZ",
theme: ThemeData(fontFamily: 'Lato'),
home: PodcastsProvider( //This is InheritedWidget
  child: RecentPodcasts(), //This is the child of InheritedWidget
 ),
));

Here's the InheritedWidget PodcastsProvider:

class PodcastsProvider extends InheritedWidget{

    final PodcastsBloc bloc;  //THIS IS THE BLOC

    PodcastsProvider({Key key, Widget child})
    :   bloc = PodcastsBloc(),
    super(key: key, child: child);

    @override
    bool updateShouldNotify(InheritedWidget oldWidget) {
      return true;
    }

    static PodcastsBloc of(BuildContext context){
      return (context.inheritFromWidgetOfExactType(PodcastsProvider) as 
PodcastsProvider).bloc;
    }
}

Here's the Bloc

class PodcastsBloc{

    var _podcasts = PublishSubject<List<Podcast>>();

    Observable<List<Podcast>> get podcasts =>_podcasts.stream;

    getPodcasts(pageCount) async{
      NetworkProvider provider = NetworkProvider();
      var podcasts = await provider.getRecentPodcasts(pageCount);
     _podcasts.sink.add(podcasts);
    }

    despose(){
      _podcasts.close();
    }
}

Here's the view part (child of InheritedWidget)

class RecentPodcasts extends StatefulWidget {
   @override
  _RecentPodcastsState createState() => _RecentPodcastsState();
}

class _RecentPodcastsState extends State<RecentPodcasts> {
   ScrollController controller = ScrollController();
   PodcastsBloc podcastsBloc;
   bool isLoading = false;
   List<Podcast> podcasts;

   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
     podcastsBloc = PodcastsProvider.of(context);
     podcastsBloc.getPodcasts(null);
     controller.addListener((){
     if(controller.position.pixels == controller.position.maxScrollExtent && !isLoading){
       setState(() {
         isLoading = true;
         podcastsBloc.getPodcasts(podcasts[podcasts.length-1].id);
       });
      }
     }); //listerner ends
   }

Finally, build method of _RecentPodcastsState calls this:

Widget getRecentPodcastsList(PodcastsBloc podcastsBloc) {

return StreamBuilder(
  stream: podcastsBloc.podcasts,
  builder: (context, snapshot) {
    //isLoading = false;
    if (snapshot.hasData) {

      podcasts.addAll(snapshot.data); //THIS IS A PROBLEM, THIS GIVES ME AN ERROR: flutter: Tried calling: addAll(Instance(length:20) of '_GrowableList')

      return ListView.builder(
          scrollDirection: Axis.vertical,
          padding: EdgeInsets.zero,
          controller: controller,
          itemCount: podcasts.length,
          itemBuilder: (context, index) {
            return RecentPodcastListItem(podcasts[index]);
          });
    } else if (snapshot.hasError) {
      //SHOW ERROR TEXT
      return Text("Error!");
    } else {
      //LOADER GOES HERE
      return Text(
        "Loading...",
        style: TextStyle(color: Colors.white),
      );
    }
  },
);
}}
like image 206
Suyash Dixit Avatar asked Dec 11 '18 18:12

Suyash Dixit


People also ask

How do I display list dynamic flutter?

Dynamic ListView You can make a dynamically created ListView by using the ListView. builder() constructor. This will create the ListView items only when they need to be displayed on the screen. It works like an Android RecyclerView but is a lot easier to set up.


1 Answers

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.
like image 82
Marcel Avatar answered Oct 06 '22 17:10

Marcel