Good use of FetchMore GQL for infinite scroll pagination ListView with Flutter

1,086

One problem here is that you're calling _scrollController.addListener() within the build function, so you are adding a listener every time the view rebuilds. This is why all the earlier fetchMore(opts) calls are triggered every time maxScrollExtent is reached.

You could move your addListener call to a more appropriate lifecycle method like initState, but I would actually recommend using a NotificationListener<ScrollUpdateNotification> which is more declarative and may suite your current code better.

Unrelatedly, you might want to check for an empty "stop" cursor to prevent infinite empty last page requests (though maybe not if your list can grow and your backend accounts for it).

Share:
1,086
poka
Author by

poka

My Gitea repositories: https://git.p2p.legal

Updated on December 26, 2022

Comments

  • poka
    poka over 1 year

    I'm trying to create a ListView of elements comming from a GraphQL API with graphql-flutter library.

    The build of the first page is OK. Then when I start to scroll, the result will be good, my ListView show the second page, then third ect ... correctly.

    The problem is, from the second page loading, there is a problem. FetchMore function seems to build multiple request at each page loading. It's like all previous page ever load are load again, more and more.

    Strangely as I said, the result of the displayed list is correct for several pages, then suddenly the list loops back from a previous page, then resumes further ...

    This is my Query widget:

    Query(
      options: QueryOptions(
        document: gql(getHistory),
        variables: <String, dynamic>{
          'pubkey': this.pubkey,
          'number': nRepositories,
          'cursor': null
        },
      ),
      builder: (QueryResult result, {refetch, FetchMore fetchMore}) {
        if (result.isLoading && result.data == null) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
    
        if (result.hasException) {
          return Text('\nErrors: \n  ' + result.exception.toString());
        }
    
        if (result.data == null && result.exception.toString() == null) {
          return const Text('Both data and errors are null');
        }
    
        final List<dynamic> blockchainTX =
            (result.data['txsHistoryBc']['both']['edges'] as List<dynamic>);
    
        final Map pageInfo =
            result.data['txsHistoryBc']['both']['pageInfo'];
    
        final String fetchMoreCursor =
            pageInfo['endCursor'];
    
        FetchMoreOptions opts = FetchMoreOptions(
          variables: {'cursor': fetchMoreCursor},
          updateQuery: (previousResultData, fetchMoreResultData) {
            final List<dynamic> repos = [
              ...previousResultData['txsHistoryBc']['both']['edges']
                  as List<dynamic>,
              ...fetchMoreResultData['txsHistoryBc']['both']['edges']
                  as List<dynamic>
            ];
    
            fetchMoreResultData['txsHistoryBc']['both']['edges'] = repos;
            return fetchMoreResultData;
          },
        );
    
        _scrollController
          ..addListener(() {
            if (_scrollController.position.pixels ==
                _scrollController.position.maxScrollExtent) {
              if (!result.isLoading) {
                print(
                    "DEBUG fetchMoreCursor in scrollController: $fetchMoreCursor");
                fetchMore(opts);
              }
            }
          });
    
        print(
            "###### DEBUG Parse blockchainTX list. Cursor: $fetchMoreCursor ######");
        List _transBC = parseHistory(blockchainTX);
    
        return Expanded(
          child: HistoryListView(
              scrollController: _scrollController,
              transBC: _transBC,
              historyData: result),
        );
      },
    ),
    

    Then the Widget HistoryListView build the list from transBC data. This is the print result of DEBUG lines (see the code above) for just one page loading, from the start of the app:

    I/flutter ( 8745): ###### DEBUG Parse blockchainTX list. Cursor: 386237:8A98F83A120EF89FC65CF43BEE77068F3DA5734340B7987FA059D582A06934F8 ######
    I/flutter ( 8745): DEBUG fetchMoreCursor in scrollController: 386237:8A98F83A120EF89FC65CF43BEE77068F3DA5734340B7987FA059D582A06934F8
    I/flutter ( 8745): ###### DEBUG Parse blockchainTX list. Cursor: 386237:8A98F83A120EF89FC65CF43BEE77068F3DA5734340B7987FA059D582A06934F8 ######
    I/flutter ( 8745): DEBUG fetchMoreCursor in scrollController: 386237:8A98F83A120EF89FC65CF43BEE77068F3DA5734340B7987FA059D582A06934F8
    I/flutter ( 8745): ###### DEBUG Parse blockchainTX list. Cursor: 386237:8A98F83A120EF89FC65CF43BEE77068F3DA5734340B7987FA059D582A06934F8 ######
    I/flutter ( 8745): ###### DEBUG Parse blockchainTX list. Cursor: 384695:4BF72317A538FB37F71C0A8D5CC36F319F87B7421260F128EFFF75B9A17C2CC7 ######
    I/flutter ( 8745): ###### DEBUG Parse blockchainTX list. Cursor: 384695:4BF72317A538FB37F71C0A8D5CC36F319F87B7421260F128EFFF75B9A17C2CC7 ######
    

    Note that the scrollController function is executed twice, with the first cursor as the same value. And the parseHistory function is then executed 3 times! I just scrolled to load 1 page here.

    I really don't understand this behavior ... I've been working on it for a week now, if developers of this library could explain it to me that would be fantastic.

    I am using version 4.0.0-beta.6 of the graphql_flutter library.


    Edit: This is the UI asociate for this test: enter image description here

    The value of nRepository here is 3.