poka
poka

Reputation: 309

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

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.

Upvotes: 5

Views: 2748

Answers (2)

Arkar Min Tun
Arkar Min Tun

Reputation: 655

I am using notification listener for capturing scroll reaching to the bottom event and it is working. And if you wanna make sure to keep the scroll position after fetching more, be sure to add key in the listview.

return NotificationListener(
  onNotification: (t) {
    if (t is ScrollEndNotification && _scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.7) {
       fetchMore(_fetchMoreOptions);
    }
    return true;
  },
  child: ListView.builder(
    key: const PageStorageKey<String>('uniqueString'),
    controller: _scrollController,
    ...
  ),
);

Upvotes: 0

micimize
micimize

Reputation: 944

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).

Upvotes: 5

Related Questions