【问题标题】:Flutter ListView lazy loadingFlutter ListView 延迟加载
【发布时间】:2018-09-05 14:14:10
【问题描述】:

如何实现无限列表视图的项目延迟加载?当用户滚动到列表视图的末尾时,我想通过网络加载更多项目。

【问题讨论】:

  • 使用ListView.builder() 并提供比已加载内容更大的项目数。当用户滚动到尚未加载的索引时,您会加载数据并重新渲染列表。
  • @GünterZöchbauer 我不喜欢这种方法,因为使用分页要困难得多。例如,您不知道未知索引是否在页面 n+1 或 n+2 内。
  • 这可能取决于用例。它在我们的项目中运行良好。您也可以将其与滚动位置结合使用。我喜欢的是我可以拥有比加载的项目更大的数量,并且滚动条反映了这一点。
  • @GünterZöchbauer 你能举个例子吗?

标签: flutter


【解决方案1】:

您可以收听ScrollController

ScrollController 有一些有用的信息,例如滚动偏移量和ScrollPosition 的列表。

在您的情况下,有趣的部分在 controller.position 中,这是当前可见的 ScrollPosition。代表可滚动的一段。

ScrollPosition 包含有关它在可滚动中的位置的信息。比如extentBeforeextentAfter。或者它的大小,extentInside

考虑到这一点,您可以触发基于extentAfter 的服务器调用,它表示剩余的可用滚动空间。

这是一个使用我所说的基本示例。

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'));
      });
    }
  }
}

您可以清楚地看到,当到达滚动条的末尾时,由于加载了更多的项目,它会消耗滚动条。

【讨论】:

  • 是的,它看起来不错,我会试试的。但我有疑问。当 items.length 发生变化时 itemCount(itemBuilder 参数)会动态变化吗?
  • 顺便说一句,我应该总是在函数体的末尾调用覆盖函数的“超级”调用吗?这是一些离顶的,但我很感兴趣)
  • @RémiRousselet 一个小技巧,super.initState() 应该始终是initState() 方法中的第一行。让我更新你的答案。
  • 这难道不是在每次加载新项目/过度滚动时重建 ListView 并因此也重建已加载项目的小部件吗?既然调用了setState,应该再次触发build,重建ListView.build?
  • @RémiRousselet 这个条件 -> controller.position.extentAfter
【解决方案2】:

感谢 Rémi Rousselet 的方法,但它并不能解决所有问题。特别是当 ListView 滚动到底部时,它仍然会调用 scrollListener 几次。改进的方法是将 Notification Listener 与 Remi 的方法结合起来。这是我的解决方案:

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));
}

【讨论】:

  • ScrollNotificationnotification.metrics.extentAfter,所以你不需要使用ScrollController。此外,当我第一次使用这两种方法时,直到我从 ListView 中删除 _controller 时它才起作用。
  • 我厌倦了我的函数被调用了几次。感谢您的解决方案
  • 可能由于版本不同,您不需要带有此代码的滚动控制器。 bool _handleScrollNotification(ScrollNotification notification) { if (notification is ScrollEndNotification &amp;&amp; notification.metrics.extentAfter == 0) { loadMore(); } return false; }
【解决方案3】:

解决方案使用 ScrollController,我看到 cmets 提到了 page。
我想分享我对 package incrementally_loading_listview 的发现 https://github.com/MaikuB/incrementally_loading_listview
正如打包所说:这可用于加载从 API 请求接收到的分页数据。

基本上,当 ListView 构建最后一个项目时,这意味着用户已经向下滚动到底部。
希望它可以帮助有类似问题的人。

出于演示的目的,我更改了示例以让页面仅包含一项 并添加一个 CircularProgressIndicator。

...
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);
              },
            );

完整示例https://github.com/MaikuB/incrementally_loading_listview/blob/master/example/lib/main.dart

包使用 ListView index = last item 和 loadMoreOffsetFromBottom 来检测何时加载更多。

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

【讨论】:

    【解决方案4】:

    这是我的方法,灵感来自上面的答案,

    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;
      }
    

    【讨论】:

      【解决方案5】:

      这是我查找列表视图结尾的解决方案

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

      如果你想在 1/2 或 3/4 之后延迟加载,可以这样改。

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

      【讨论】:

        【解决方案6】:

        使用lazy_load_scrollview:1.0.0 包,它在熊猫世界在这里回答的幕后使用相同的概念。该软件包使其更易于实施。

        【讨论】:

        • 我使用的是同一个包,它工作正常。我只是想知道当数据仍在加载时,我们如何在List 的末尾显示CircularProgressIndicator,就像当我们到达List 的末尾时,我们将从API 中获取一些数据,直到我们得到数据,我们只显示CircularProgressIndicator
        【解决方案7】:

        接受的答案是正确的,但你也可以这样做,

        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();
              },
            );
          }
        

        【讨论】:

          【解决方案8】:

          如果您想实现上下方向的延迟加载,发布的解决方案并不能解决问题。滚动会跳到这里,见this thread

          如果你想在上下方向进行延迟加载,库 bidirectional_listview 可以提供帮助。

          示例 (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;
          }
          

          我希望这可以帮助一些人:-)

          【讨论】:

            【解决方案9】:

            还有这个包,去掉样板:https://pub.dev/packages/lazy_load_scrollview

            【讨论】:

              猜你喜欢
              • 2020-04-09
              • 1970-01-01
              • 1970-01-01
              • 2022-11-07
              • 1970-01-01
              • 2021-10-17
              • 2021-11-25
              • 1970-01-01
              相关资源
              最近更新 更多