Flutter ListView 延迟加载
Flutter ListView lazy loading
无限listview如何实现item懒加载?当用户滚动到列表视图的末尾时,我想通过网络加载更多项目。
你可以听一首ScrollController
。
ScrollController
有一些有用的信息,例如 scrolloffset 和 ScrollPosition
.
的列表
在您的情况下,有趣的部分在 controller.position
中,这是当前可见的 ScrollPosition
。表示可滚动的一段。
ScrollPosition
包含有关它在可滚动内容中的位置的信息。如extentBefore
和extentAfter
。或者它的大小,extentInside
.
考虑到这一点,您可以根据 extentAfter
触发服务器调用,它表示剩余的滚动 space 可用。
这是一个使用我所说内容的基本示例。
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'));
});
}
}
}
你可以清楚地看到,当到达滚动条的末尾时,由于加载了更多的项目,滚动条会膨胀。
感谢 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));
}
解决方案使用 ScrollController,我看到了关于页面的评论。
我想分享我对包裹的发现 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);
}
使用 lazy_load_scrollview: 1.0.0 包,在后台使用与熊猫世界在这里回答的相同的概念。该软件包使其更易于实施。
如果你想实现上下方向的延迟加载,发布的解决方案并不能解决问题。滚动会跳到这里,参见 .
如果你想在上下方向上进行延迟加载,库 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;
}
我希望这对一些人有帮助:-)
这是我的方法,其灵感来自于上述答案,
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;
}
这是我找到 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();
}
接受的答案是正确的,但您也可以按照以下方式进行操作,
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();
},
);
}
还有这个包,带走了样板:https://pub.dev/packages/lazy_load_scrollview
无限listview如何实现item懒加载?当用户滚动到列表视图的末尾时,我想通过网络加载更多项目。
你可以听一首ScrollController
。
ScrollController
有一些有用的信息,例如 scrolloffset 和 ScrollPosition
.
在您的情况下,有趣的部分在 controller.position
中,这是当前可见的 ScrollPosition
。表示可滚动的一段。
ScrollPosition
包含有关它在可滚动内容中的位置的信息。如extentBefore
和extentAfter
。或者它的大小,extentInside
.
考虑到这一点,您可以根据 extentAfter
触发服务器调用,它表示剩余的滚动 space 可用。
这是一个使用我所说内容的基本示例。
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'));
});
}
}
}
你可以清楚地看到,当到达滚动条的末尾时,由于加载了更多的项目,滚动条会膨胀。
感谢 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));
}
解决方案使用 ScrollController,我看到了关于页面的评论。
我想分享我对包裹的发现 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);
}
使用 lazy_load_scrollview: 1.0.0 包,在后台使用与熊猫世界在这里回答的相同的概念。该软件包使其更易于实施。
如果你想实现上下方向的延迟加载,发布的解决方案并不能解决问题。滚动会跳到这里,参见
如果你想在上下方向上进行延迟加载,库 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;
}
我希望这对一些人有帮助:-)
这是我的方法,其灵感来自于上述答案,
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;
}
这是我找到 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();
}
接受的答案是正确的,但您也可以按照以下方式进行操作,
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();
},
);
}
还有这个包,带走了样板:https://pub.dev/packages/lazy_load_scrollview