Flutter ListView lazy loading

Flutter

Flutter Problem Overview


How can I realize items lazy loading for endless listview? I want to load more items by network when user scroll to the end of listview.

Flutter Solutions


Solution 1 - Flutter

You can listen to a ScrollController.

ScrollController has some useful information, such as the scrolloffset and a list of ScrollPosition.

In your case the interesting part is in controller.position which is the currently visible ScrollPosition. Which represents a segment of the scrollable.

ScrollPosition contains informations about it's position inside the scrollable. Such as extentBefore and extentAfter. Or it's size, with extentInside.

Considering this, you could trigger a server call based on extentAfter which represents the remaining scroll space available.

Here's an basic example using what I said.

class MyHome extends StatefulWidget {
  @override
  _MyHomeState createState() => _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
  ScrollController controller;
  List<String> items = List.generate(100, (index) => 'Hello $index');

  @override
  void initState() {
    super.initState();
    controller = ScrollController()..addListener(_scrollListener);
  }

  @override
  void dispose() {
    controller.removeListener(_scrollListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Scrollbar(
        child: ListView.builder(
          controller: controller,
          itemBuilder: (context, index) {
            return Text(items[index]);
          },
          itemCount: items.length,
        ),
      ),
    );
  }

  void _scrollListener() {
    print(controller.position.extentAfter);
    if (controller.position.extentAfter < 500) {
      setState(() {
        items.addAll(List.generate(42, (index) => 'Inserted $index'));
      });
    }
  }
}

You can clearly see that when reaching the end of the scroll, it scrollbar expends due to having loaded more items.

Solution 2 - Flutter

Thanks for Rémi Rousselet's approach, but it does not solve all the problem. Especially when the ListView has scrolled to the bottom, it still calls the scrollListener a couple of times. The improved approach is to combine Notification Listener with Remi's approach. Here is my solution:

bool _handleScrollNotification(ScrollNotification notification) {
  if (notification is ScrollEndNotification) {
    if (_controller.position.extentAfter == 0) {
      loadMore();
    }
  }
  return false;
}

@override
Widget build(BuildContext context) {
    final Widget gridWithScrollNotification = NotificationListener<
            ScrollNotification>(
        onNotification: _handleScrollNotification,
        child: GridView.count(
            controller: _controller,
            padding: EdgeInsets.all(4.0),
          // Create a grid with 2 columns. If you change the scrollDirection to
          // horizontal, this would produce 2 rows.
          crossAxisCount: 2,
          crossAxisSpacing: 2.0,
          mainAxisSpacing: 2.0,
          // Generate 100 Widgets that display their index in the List
          children: _documents.map((doc) {
            return GridPhotoItem(
              doc: doc,
            );
          }).toList()));
    return new Scaffold(
      key: _scaffoldKey,
      body: RefreshIndicator(
       onRefresh: _handleRefresh, child: gridWithScrollNotification));
}

Solution 3 - Flutter

The solution use ScrollController and I saw comments mentioned about page.
I would like to share my finding about package incrementally_loading_listview https://github.com/MaikuB/incrementally_loading_listview.
As packaged said : This could be used to load paginated data received from API requests.

Basically, when ListView build last item and that means user has scrolled down to the bottom.
Hope it can help someone who have similar questions.

For purpose of demo, I have changed example to let a page only include one item and add an CircularProgressIndicator.

enter image description here

...
bool _loadingMore;
bool _hasMoreItems;
int  _maxItems = 30;
int  _numItemsPage = 1;
...
_hasMoreItems = items.length < _maxItems;    
...
return IncrementallyLoadingListView(
              hasMore: () => _hasMoreItems,
              itemCount: () => items.length,
              loadMore: () async {
                // can shorten to "loadMore: _loadMoreItems" but this syntax is used to demonstrate that
                // functions with parameters can also be invoked if needed
                await _loadMoreItems();
              },
              onLoadMore: () {
                setState(() {
                  _loadingMore = true;
                });
              },
              onLoadMoreFinished: () {
                setState(() {
                  _loadingMore = false;
                });
              },
              loadMoreOffsetFromBottom: 0,
              itemBuilder: (context, index) {
                final item = items[index];
                if ((_loadingMore ?? false) && index == items.length - 1) {
                  return Column(
                    children: <Widget>[
                      ItemCard(item: item),
                      Card(
                        child: Padding(
                          padding: const EdgeInsets.all(16.0),
                          child: Column(
                            children: <Widget>[
                              Row(
                                crossAxisAlignment:
                                    CrossAxisAlignment.start,
                                children: <Widget>[
                                  Container(
                                    width: 60.0,
                                    height: 60.0,
                                    color: Colors.grey,
                                  ),
                                  Padding(
                                    padding: const EdgeInsets.fromLTRB(
                                        8.0, 0.0, 0.0, 0.0),
                                    child: Container(
                                      color: Colors.grey,
                                      child: Text(
                                        item.name,
                                        style: TextStyle(
                                            color: Colors.transparent),
                                      ),
                                    ),
                                  )
                                ],
                              ),
                              Padding(
                                padding: const EdgeInsets.fromLTRB(
                                    0.0, 8.0, 0.0, 0.0),
                                child: Container(
                                  color: Colors.grey,
                                  child: Text(
                                    item.message,
                                    style: TextStyle(
                                        color: Colors.transparent),
                                  ),
                                ),
                              )
                            ],
                          ),
                        ),
                      ),
                      Center(child: CircularProgressIndicator())
                    ],
                  );
                }
                return ItemCard(item: item);
              },
            );

full example https://github.com/MaikuB/incrementally_loading_listview/blob/master/example/lib/main.dart

Package use ListView index = last item and loadMoreOffsetFromBottom to detect when to load more.

    itemBuilder: (itemBuilderContext, index) {    
              if (!_loadingMore &&
              index ==
                  widget.itemCount() -
                      widget.loadMoreOffsetFromBottom -
                      1 &&
              widget.hasMore()) {
            _loadingMore = true;
            _loadingMoreSubject.add(true);
          }

Solution 4 - Flutter

here is my approach which is inspired by answers above,

NotificationListener(onNotification: _onScrollNotification, child: GridView.builder())

bool _onScrollNotification(ScrollNotification notification) {
    if (notification is ScrollEndNotification) {
      final before = notification.metrics.extentBefore;
      final max = notification.metrics.maxScrollExtent;

      if (before == max) {
        // load next page
        // code here will be called only if scrolled to the very bottom
      }
    }
    return false;
  }

Solution 5 - Flutter

here is my solution for find end of listView

_scrollController.addListener(scrollListenerMilli);


if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
      getMoreData();
    }

If you want to lazy load after 1/2 or 3/4, change like this.

if (_scrollController.position.pixels == (_scrollController.position.maxScrollExtent * .75)) {//.5
      getMoreData();
    }

Solution 6 - Flutter

Use lazy_load_scrollview: 1.0.0 package that use same concept behind the scenes that panda world answered here. The package make it easier to implement.

Solution 7 - Flutter

The solutions posted don't solve the issue if you want to achieve lazy loading in up AND down direction. The scrolling would jump here, see this thread.

If you want to do lazy loading in up and down direction, the library bidirectional_listview could help.

Example (Source):

static const double kItemHeight = 30.0;
BidirectionalScrollController controller;
double oldScrollPosition = 0.0;

@override
void initState() {
  super.initState();

  for (int i = -10; i <= 10; i++) {
    items[i] = "Item " + i.toString();
  }

  controller = new BidirectionalScrollController()
    ..addListener(_scrollListener);
}
@override
void dispose() {
  controller.removeListener(_scrollListener);
  super.dispose();
}

@override
void build() {
// ...
  List<int> keys = items.keys.toList();
  keys.sort();
  
  return new BidirectionalListView.builder(
    controller: controller,
    physics: AlwaysScrollableScrollPhysics(),
    itemBuilder: (context, index) {
      return Container(
          child: Text(items[index]),
          height: kItemHeight,
    },
    itemCount: keys.first,
    negativeItemCount: keys.last.abs(),
  );
// ...
}

// Reload new items in up and down direction and update scroll boundaries
void _scrollListener() {
  bool scrollingDown = oldScrollPosition < controller.position.pixels;
  List<int> keys = items.keys.toList();
  keys.sort();
  int negativeItemCount = keys.first.abs();
  int itemCount = keys.last;
 
  double positiveReloadBorder = (itemCount * kItemHeight - 3 * kItemHeight);
  double negativeReloadBorder =
      (-(negativeItemCount * kItemHeight - 3 * kItemHeight));
 
  // reload items
  bool rebuildNecessary = false;
  if (scrollingDown && controller.position.pixels > positiveReloadBorder) 
  {
    for (int i = itemCount + 1; i <= itemCount + 20; i++) {
      items[i] = "Item " + i.toString();
    }
    rebuildNecessary = true;
  } else if (!scrollingDown &&
      controller.position.pixels < negativeReloadBorder) {
    for (int i = -negativeItemCount - 20; i < -negativeItemCount; i++) {
      items[i] = "Item " + i.toString();
    }
    rebuildNecessary = true;
  }
 
  // set new scroll boundaries
  try {
    BidirectionalScrollPosition pos = controller.position;
    pos.setMinMaxExtent(
        -negativeItemCount * kItemHeight, itemCount * kItemHeight);
  } catch (error) {
    print(error.toString());
  }
  if (rebuildNecessary) {
    setState(({});
  }
 
  oldScrollPosition = controller.position.pixels;
}

I hope that this helps a few people :-)

Solution 8 - Flutter

The accepted answer is correct but you can also do as follows,

Timer _timer;

  Widget chatMessages() {
    _timer = new Timer(const Duration(milliseconds: 300), () {
      _scrollController.animateTo(
        _scrollController.position.maxScrollExtent,
        curve: Curves.easeOut,
        duration: const Duration(milliseconds: 300),
      );
    });
    return StreamBuilder(
      stream: chats,
      builder: (context, snapshot) {
        return snapshot.hasData
            ? ListView.builder(
                // physics: NeverScrollableScrollPhysics(),
                controller: _scrollController,
                shrinkWrap: true,
                reverse: false,
                itemCount: snapshot.data.documents.length,
                itemBuilder: (context, index) {
                  return MessageTile(
                    message: snapshot.data.documents[index].data["message"],
                    sendByMe: widget.sendByid ==
                        snapshot.data.documents[index].data["sendBy"],
                  );
                })
            : Container();
      },
    );
  }

Solution 9 - Flutter

There is also this package, taking away the boilerplate: https://pub.dev/packages/lazy_load_scrollview

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionValentin SchukinView Question on Stackoverflow
Solution 1 - FlutterRémi RousseletView Answer on Stackoverflow
Solution 2 - FlutterPanda WorldView Answer on Stackoverflow
Solution 3 - FlutterchunhunghanView Answer on Stackoverflow
Solution 4 - FlutterOmar AmarView Answer on Stackoverflow
Solution 5 - FlutterBIS TechView Answer on Stackoverflow
Solution 6 - FlutterMd Sadab WasimView Answer on Stackoverflow
Solution 7 - FlutterChrisRoView Answer on Stackoverflow
Solution 8 - FluttergsmView Answer on Stackoverflow
Solution 9 - FlutterAbdelghani BekkaView Answer on Stackoverflow