1

my use case is to create a list view of articles (each item have the same look, there could be huge amount of articles, e.g. > 10000). I tried with - ListView with ListView.builder: it supposes only to render the item when the item is displayed - ScrollController: to determine when to load the next items (pagination) - then I use List to store the data fetched from restful API using http, by adding the data from http to the List instance

this approach is OK, but in case the user keeps on scrolling pages, the List instance will have more and more items, it can crash with stack Overflow error.

If I don't call List.addAll(), instead I assign the data fetched from api, like: list = data; I have problem that when the user scroll up, he/she won't be able to see the previous items.

Is there a good approach to solve this? Thanks!

  import 'package:flutter/material.dart';
  import 'package:app/model.dart';
  import 'package:app/components/item.dart';

  abstract class PostListPage extends StatefulWidget {
    final String head;
    DealListPage(this.head);
  }

  abstract class PostListPageState<T extends PostListPage> extends State<PostListPage> {
    final int MAX_PAGE = 2;

    DealListPageState(String head) {
      this.head = head;
    }

    final ScrollController scrollController = new ScrollController();

    void doInitialize() {

      page = 0;
      try {
        list.clear();
        fetchNextPage();
      }
      catch(e) {
        print("Error: " + e.toString());
      }
    }

    @override
    void initState() {
      super.initState();
      this.fetchNextPage();
      scrollController.addListener(() {
        double maxScroll = scrollController.position.maxScrollExtent;
        double currentScroll = scrollController.position.pixels;
        double delta = 200.0; // or something else..
        if ( maxScroll - currentScroll <= delta) {
          fetchNextPage();
        }
      });
    }

    @override
    void dispose() {
      scrollController.dispose();
      super.dispose();
    }


    void mergeNewResult(List<PostListItem> result) {
      list.addAll(result);

    }


    Future fetchNextPage() async {
      if (!isLoading && mounted) {
        page++;
        setState(() {
          isLoading = true;
        });
        final List<PostListItem> result = await doFetchData(page);
        setState(() {
          if (result != null && result.length > 0) {
            mergeNewResult(result);
          } else {
          //TODO show notification
          }
          isLoading = false;
        });
      }
    }

    Future doFetchData(final int page);

    String head;
    List<PostListItem> list = new List();
    var isLoading = false;

    int page = 0;
    int pageSize = 20;
    final int scrollThreshold = 10;

    Widget buildProgressIndicator() {
      return new Padding(
        padding: const EdgeInsets.all(8.0),
        child: new Center(
          child: new Opacity(
            opacity: isLoading ? 1.0 : 0.0,
            child: new CircularProgressIndicator(),
          ),
        ),
      );
    }

    @override
    Widget build(BuildContext context) {
      ListView listView = ListView.builder(
        padding: const EdgeInsets.all(16.0),
        itemBuilder: (BuildContext context, int index) {

          if (index == list.length) {
            return buildProgressIndicator();
          }

          if (index > 0) {
            return Column(
                children: [Divider(), PostListItem(list[index])]
            );
          }
          return PostListItem(list[index]);
        },
        controller: scrollController,
        itemCount: list.length
    );

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
          title: Text(head),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.search),
              onPressed: () {

              },
            ),
            // action button
            IconButton(
              icon: Icon(Icons.more_horiz),
              onPressed: () {
              },
            ),
          ]
        ),
        body: new RefreshIndicator(
          onRefresh: handleRefresh,
          child: listView
        ),

      );
    }

    Future<Null> handleRefresh() async {
      doInitialize();
      return null;
    }
  }

in my case, when the list length is 600, I start to get stack overflow error like:

I/flutter ( 8842): Another exception was thrown: Stack Overflow
I/flutter ( 8842): Another exception was thrown: Stack Overflow

screen:

enter image description here

somehow flutter doesn't show any more details of the error.

Michael
  • 11
  • 3
  • simply get your paged data in `ListView.builder#itemBuilder` (use `MapCache` for asynchronous data access) - that way you will request the data on demand when you scroll your `ListView` – pskink Dec 31 '18 at 14:53
  • @pskink: thanks! but how to set the itemCount parameter in this case? do you have some sample code? I was wondering that itemBuilder() method actually is to build the item, but not sure if it is the right place to change the list. – Michael Dec 31 '18 at 16:23
  • you can either specify the count if you know it or return null from the widget builder - something like [this](https://codeshare.io/5X4AXY) – pskink Dec 31 '18 at 16:31
  • Where does the stack overflow occur? Can you add the stack trace? I would expect some kind of "out of memory" error, but not a stack overflow. – boformer Dec 31 '18 at 16:45
  • @boformer: I attached the screen and the error. somehow there are not much details from flutter. – Michael Jan 01 '19 at 09:07
  • @pskink: yeah, it looks good. I will try this approach, but probably with cache based on file. – Michael Jan 01 '19 at 09:09
  • so all you need to do is to provide your own "map" passed to `MapCache` constructor: `MapCache(map: map)` - in my case it was simple: `Map> map = {};` – pskink Jan 01 '19 at 09:24
  • It says "another exception". the first log entry should contain more details – boformer Jan 01 '19 at 11:39
  • How about that: https://stackoverflow.com/questions/60074466/pagination-infinite-scrolling-in-flutter-with-caching-and-realtime-invalidatio – unveloper Feb 25 '20 at 10:08

1 Answers1

1

I wrote some sample code for a related question about paginated scrolling, which you could check out.

I didn't implement cache invalidation there, but it would easily be extendable using something like the following in the getPodcast method to remove all items that are more than 100 indexes away from the current location:

for (key in _cache.keys) {
  if (abs(key - index) > 100) {
    _cache.remove(key);
  }
}

An even more sophisticated implementation could take into consideration the scroll velocity and past user behavior to lay out a probability curve (or a simpler Gaussian curve) to fetch content more intelligently.

Marcel
  • 8,831
  • 4
  • 39
  • 50