0

I'm trying to implement some endless scroll pagination in a SingleChildScrollView.

I'm setting 2 const values:

///describes how many items are loaded into right away
const int initialWidgetsLoadCount = 3;
///describes how many items are loaded after the initial load
///(when triggered by reaching the end of the SingleChildScrollView)
const int nextWidgetsLoadCount = 1;

For this minimal example I'm just filling up a Column with Text widgets:

SingleChildScrollView(
    controller: _scrollController,
    child: Column(children: _texts),
)

For adding Text widgets I use this method:

void _addTextWidgets(int numberOfWidgets) {
  setState(() {
    for (int i = 0; i < numberOfWidgets; i++) {
      _texts.add(Text("someText${++counter}"));
    }
  });
}

Now for making the pagination work, in initState() I first add some initial widgets and start listening to the scrollbar reaching the bottom to load more:

@override
void initState() {
  //this is the initial fill request
  _addTextWidgets(initialWidgetsLoadCount);

  _scrollController.addListener(() {
    if (_scrollController.position.maxScrollExtent ==
        _scrollController.offset) {
      //the bottom of the scrollbar is reached
      //adding more widgets
      _addTextWidgets(nextWidgetsLoadCount);
    }
  });
  super.initState();
} 

The problem:

If I set the initialWidgetsLoadCount to a high enough number, the height of the Column will be large enough for the ScrollBar of the SingeChildScrollView to appear. Then everything works fine. However, if I set initialWidgetsLoadCount to a lower number (e.g. 1), the Column height will be too small for the ScrollBar to appear. Therefore the end of the SingeChildScrollView cannot be reached by the user and no more items will be loaded.

I could set the initialWidgetsLoadCount to a high enough number for my device, but there might be a device with a huge resolution.

So I'm tring to figure out a way to tell if the ScrollBar is there or not. And while the ScrollBar is not there yet I can keep loading the initialWidgetsLoadCount of widgets.

Note: The Add-Button in this example is just for testing purposes! In a real environment I want the user to be able to load all items without having to use a FloatingActionButton, even with very small values for initialWidgetsLoadCount.

Nother Note: The size of the list is unknown. I keep fetching data until there is no more data.

Here is a DartPad of this example.

Lara
  • 84
  • 1
  • 7
  • 30
  • For a list with lots of items, use `ListView` or something alike, don't use `SingleChildScrollView`. – WSBT Apr 07 '22 at 18:28
  • agree, you should use `ListView.builder` instead of `SingleChildScrollView` – pskink Apr 08 '22 at 08:00
  • @pskink @user1032613 Don't I have the same problem with `ListView.builder` though? – Lara Apr 08 '22 at 08:16
  • All I find is examples using high numbers for initial loads so they don't have to address this issue. – Lara Apr 08 '22 at 08:27

2 Answers2

0

I found a way using the ScrollController. For this solution it does not matter whether you are using ListView.builder or SingeChildScrollView.

I changed the initState() method and put the line that adds the initial widgets after the actual super.initState() call:

  @override
  void initState() {
    _scrollController.addListener(() {
      if (_scrollController.position.maxScrollExtent ==
          _scrollController.offset) {
        //the bottom of the scrollbar is reached
        //adding more widgets
        _addTextWidgets(nextWidgetsLoadCount);
      }
    });
    super.initState();

    //after initState add the initial widgets
    _addTextWidgets(initialWidgetsLoadCount);

    _checkInitialExtent();
  }

In the _checkInitialExtent() I keep adding the initial widgets until the ScrollController tells me that it is scrollable:

void _checkInitialExtent() {
    //after the frame
    WidgetsBinding.instance?.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        debugPrint(_scrollController.position.maxScrollExtent.toString());
        //if the list is not scrollable yet
        if (_scrollController.position.maxScrollExtent == 0) {
          //add the initial widgets again
          _addTextWidgets(initialWidgetsLoadCount);
          //recursively check again
          _checkInitialExtent();
        } else {
          //finally enough widgets to make it scrollable (maxScrollExtent > 0 now)
          debugPrint("maxScrollExtent > 0 now");
        }
      }
    });
  }
Lara
  • 84
  • 1
  • 7
  • 30
-1

Any list has the end that you need to know that just calculate it inside the listener. Update your code with my example and test if matches your preferences.

Note: use ListView.builder for longer and larger lists.

/// The max list length.
const len = 200;

/// Decide the initial length.
const int initialWidgetsLoadCount = len < 15 ? len : 15;

/// Decide how many items you want to add.
const int nextWidgetsLoadCount = 15;

@override
  void initState() {
    // This is the initial fill request
    _addTextWidgets(initialWidgetsLoadCount);
    
    _scrollController.addListener(() {
      // And listen to the list length to calculate the next items load length
      if (_scrollController.position.maxScrollExtent ==
          _scrollController.offset) {
        if(counter < len) {
          if((len - counter) < nextWidgetsLoadCount) {
            _addTextWidgets(len - counter);
          } else {
            _addTextWidgets(nextWidgetsLoadCount);
          }
        }
      }
    });
    super.initState();
  }
Arnas
  • 802
  • 1
  • 6
  • 14
  • I don't think you understood the problem in my question. I added a note to it at the end now to make it clearer – Lara Apr 08 '22 at 06:29
  • Looks like you did not try my example. You can remove add button, with my example adding button is not needed, its everything based on the list length. – Arnas Apr 08 '22 at 06:34
  • If you set the initial load to reach the bottom of the scrollbar then you will activate the listener, if your list is too short anyway you don't need to activate the listener in all case – Arnas Apr 08 '22 at 06:45
  • I get that. The problem with this approach is 1. the length of the list is unknown (for example with Firestore you query the data with `query.limit(nextAmout).startAfterDocument(_lastDoc!).get();` until the result is empy. And 2. you want to be able to set the number of initial load to a low number like 1 and still don't break functionality on any device. So basically: while scrollbar is not there yet, keep loading initial load. from then on the user can keep loading more themselves. – Lara Apr 08 '22 at 06:56
  • I just give you an example of how to handle your list without the add button. From where you load the list you don't write about it. But look here https://stackoverflow.com/questions/53776613/pagination-with-listview-in-flutter – Arnas Apr 08 '22 at 07:12
  • See in your example, set the `Container` height to 1000. And it will stop working. The number 15 might be enough for many devices but not for devices and screens with extreme heights. I need it to work with the number 1, too. And yes it doesn't matter where I get the data from, i just wanted you to know that I can't tell the total number of items. – Lara Apr 08 '22 at 07:21