Flutter ListView延迟加载

时间:2018-03-27 08:49:37

标签: flutter

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

10 个答案:

答案 0 :(得分:32)

您可以收听ScrollController

ScrollController包含一些有用的信息,例如scrolloffset和ScrollPosition列表。

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

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

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

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

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

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

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

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

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

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

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

答案 1 :(得分:10)

感谢RémiRousselet的方法,但并不能解决所有问题。特别是当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));
}

答案 2 :(得分:2)

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

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

答案 3 :(得分:1)

该解决方案使用ScrollController,我看到了有关页面的评论。
我想分享一下我对增量增量包的发现_loading_listview https://github.com/MaikuB/incrementally_loading_listview
如包装所述:这可用于加载从API请求接收的分页数据。

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

出于演示目的,我更改了示例以使一个页面仅包含一项 并添加一个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);
              },
            );

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

包装使用ListView索引=最后一项,并使用loadMoreOffsetFromBottom来检测何时加载更多商品。

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

答案 4 :(得分:1)

如果要实现上下方向的延迟加载,则发布的解决方案无法解决问题。滚动会跳到此处,请参见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;
}

我希望这对一些人有帮助:-)

答案 5 :(得分:1)

这是我找到listView结尾的解决方案

_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 :(得分:1)

接受的答案是正确的,但您也可以执行以下操作,

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

答案 7 :(得分:0)

我使用https://pub.dev/packages/loadany处理更多的加载,因为他支持ListView,GridView,ScrollView

答案 8 :(得分:0)

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

答案 9 :(得分:-1)

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