Flutter ListView 双向延迟加载(上、下)
Posted
技术标签:
【中文标题】Flutter ListView 双向延迟加载(上、下)【英文标题】:Flutter ListView lazy loading in both directions (up, down) 【发布时间】:2020-04-09 20:20:02 【问题描述】:我希望在 Flutter 中有一个 ListView,它可以在两个方向(上、下)提供延迟加载。
例子:
后台数据库理论上可以显示60000条。 首先我要显示项目 100..120 从这些索引中,我希望能够在延迟加载新项目时上下滚动需要考虑的事项:
顶部和底部边缘(当前索引 60000)在到达时应该是弹跳的 滚动我尝试过的:
Flutter ListView lazy loading 中的大多数方法。这些解决方案仅适用于一个方向的延迟加载(例如,如果列表反转,则向下或向上)。如果滚动到另一个方向,列表视图会“跳跃”,因为索引发生了变化(旧索引 1 不再是新索引 1)。 ScrollablePositionedList (https://pub.dev/documentation/flutter_widgets/latest/flutter_widgets/ScrollablePositionedList-class.html):这里的问题是小部件想要加载每个项目,例如给出的 itemcount 为 60000。无论如何,需要 itemcount 才能使该解决方案正常工作。 IndexedListView (https://pub.dev/packages/indexed_list_view):与 ScrollablePositionedList 中的问题相同。无论如何,这里列表顶部和底部的“弹跳”也不起作用(因为缺少滚动范围)。 InfiniteListView (https://github.com/fluttercommunity/flutter_infinite_listview):与 IndexedListView 中的问题相同我希望这里有一些非常聪明的人可以帮助我解决这个问题;)。我已经在这个问题上搜索和尝试了几天。谢谢!
更新 为了让事情更清楚:这是一个用于上下滚动的延迟加载的 ListView 示例(大部分代码由 Rémi Rousselet 从https://***.com/a/49509349/10905712 复制):
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
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,
),
),
);
double oldScrollPosition = 0.0;
void _scrollListener()
bool scrollingDown = oldScrollPosition < controller.position.pixels;
print(controller.position.extentAfter);
if (controller.position.extentAfter < 500 && scrollingDown)
setState(()
items.addAll(new List.generate(
42, (int index) => Random().nextInt(10000).toString()));
);
else if (controller.position.extentBefore < 500 && !scrollingDown)
setState(()
items.insertAll(
0,
new List.generate(
42, (index) => Random().nextInt(10000).toString()));
);
oldScrollPosition = controller.position.pixels;
如果您执行此代码并尝试向上滚动,您会在列表中看到“跳跃”。向下滚动+延迟加载效果很好。 如果 ListView 反转,向上滚动 + 延迟加载将起作用。无论如何,使用此解决方案,我们将在此处遇到向下滚动 + 延迟加载的相同问题。
【问题讨论】:
使用MapCache.lru
- 文档说:“对缓存的所有访问都是异步的,因为许多实现会将它们的条目存储在远程系统中,隔离,或者必须执行异步 IO 才能读取并写入。” - 在ListView.builder
中使用它 - 这样您将拥有一个小“窗口”,其中包含基于异步远程数据的缓存值
这很快,谢谢!这可能有助于缓存项目,但从我的角度来看并不能解决问题。想想 ListView.builder:如果向上滚动,索引 1..x 将被新项目覆盖,而 x..x*2 将是旧项目。这将导致列表视图中的“跳转”或不流畅的滚动。这将有助于在构建器中启用“反向”选项。无论如何,那么另一个滚动方向将不再流畅。
基本上你想要的是一个带有builder
构造函数的双向ListView?
完全正确 - 在两个方向上都有延迟加载和顶部/底部检测(=滚动边界)。
如果你反向滚动(指数越来越低)构建器索引被正确调用,例如:100、99、98等
【参考方案1】:
更新
我刚刚创建了一个新库bidirectional_listview,可以用来解决这个问题。 BidirectionalListView 是来自infinite_listview 的一个分支。
旧答案
我刚刚通过调整库 InfiniteListView 解决了这个问题。我不得不为 minScrollExtent 和 maxScrollExtent 扩展一个 setter。此外,我为负索引添加了单独的计数:
library infinite_listview;
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Infinite ListView
///
/// ListView that builds its children with to an infinite extent.
///
class BidirectionalListView extends StatelessWidget
/// See [ListView.builder]
BidirectionalListView.builder(
Key key,
this.scrollDirection = Axis.vertical,
BidirectionalScrollController controller,
this.physics,
this.padding,
this.itemExtent,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
int negativeItemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
this.anchor = 0.0,
this.cacheExtent,
) : positiveChildrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
negativeChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) => itemBuilder(context, -1 - index),
childCount: negativeItemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
controller = controller ?? BidirectionalScrollController(),
super(key: key);
/// See [ListView.separated]
BidirectionalListView.separated(
Key key,
this.scrollDirection = Axis.vertical,
BidirectionalScrollController controller,
this.physics,
this.padding,
@required IndexedWidgetBuilder itemBuilder,
@required IndexedWidgetBuilder separatorBuilder,
int itemCount,
int negativeItemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
this.cacheExtent,
this.anchor = 0.0,
) : assert(itemBuilder != null),
assert(separatorBuilder != null),
itemExtent = null,
positiveChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index)
final itemIndex = index ~/ 2;
return index.isEven
? itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
,
childCount: itemCount != null ? math.max(0, itemCount * 2 - 1) : null,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
negativeChildrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index)
final itemIndex = (-1 - index) ~/ 2;
return index.isOdd
? itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
,
childCount: negativeItemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
controller = controller ?? BidirectionalScrollController(),
super(key: key);
/// See: [ScrollView.scrollDirection]
final Axis scrollDirection;
/// See: [ScrollView.controller]
final BidirectionalScrollController controller;
/// See: [ScrollView.physics]
final ScrollPhysics physics;
/// See: [BoxScrollView.padding]
final EdgeInsets padding;
/// See: [ListView.itemExtent]
final double itemExtent;
/// See: [ScrollView.cacheExtent]
final double cacheExtent;
/// See: [ScrollView.anchor]
final double anchor;
/// See: [ListView.childrenDelegate]
final SliverChildDelegate negativeChildrenDelegate;
/// See: [ListView.childrenDelegate]
final SliverChildDelegate positiveChildrenDelegate;
@override
Widget build(BuildContext context)
final List<Widget> slivers = _buildSlivers(context, negative: false);
final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
final AxisDirection axisDirection = _getDirection(context);
final scrollPhysics = AlwaysScrollableScrollPhysics(parent: physics);
return Scrollable(
axisDirection: axisDirection,
controller: controller,
physics: scrollPhysics,
viewportBuilder: (BuildContext context, ViewportOffset offset)
return Builder(builder: (BuildContext context)
/// Build negative [ScrollPosition] for the negative scrolling [Viewport].
final state = Scrollable.of(context);
final negativeOffset = BidirectionalScrollPosition(
physics: scrollPhysics,
context: state,
initialPixels: -offset.pixels,
keepScrollOffset: controller.keepScrollOffset,
negativeScroll: true,
);
/// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
offset.addListener(()
negativeOffset._forceNegativePixels(offset.pixels);
);
/// Stack the two [Viewport]s on top of each other so they move in sync.
return Stack(
children: <Widget>[
Viewport(
axisDirection: flipAxisDirection(axisDirection),
anchor: 1.0 - anchor,
offset: negativeOffset,
slivers: negativeSlivers,
cacheExtent: cacheExtent,
),
Viewport(
axisDirection: axisDirection,
anchor: anchor,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
),
],
);
);
,
);
AxisDirection _getDirection(BuildContext context)
return getAxisDirectionFromAxisReverseAndDirectionality(
context, scrollDirection, false);
List<Widget> _buildSlivers(BuildContext context, bool negative = false)
Widget sliver;
if (itemExtent != null)
sliver = SliverFixedExtentList(
delegate:
negative ? negativeChildrenDelegate : positiveChildrenDelegate,
itemExtent: itemExtent,
);
else
sliver = SliverList(
delegate:
negative ? negativeChildrenDelegate : positiveChildrenDelegate);
if (padding != null)
sliver = new SliverPadding(
padding: negative
? padding - EdgeInsets.only(bottom: padding.bottom)
: padding - EdgeInsets.only(top: padding.top),
sliver: sliver,
);
return <Widget>[sliver];
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties)
super.debugFillProperties(properties);
properties.add(new EnumProperty<Axis>('scrollDirection', scrollDirection));
properties.add(new DiagnosticsProperty<ScrollController>(
'controller', controller,
showName: false, defaultValue: null));
properties.add(new DiagnosticsProperty<ScrollPhysics>('physics', physics,
showName: false, defaultValue: null));
properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>(
'padding', padding,
defaultValue: null));
properties
.add(new DoubleProperty('itemExtent', itemExtent, defaultValue: null));
properties.add(
new DoubleProperty('cacheExtent', cacheExtent, defaultValue: null));
/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
class BidirectionalScrollController extends ScrollController
/// Creates a new [BidirectionalScrollController]
BidirectionalScrollController(
double initialScrollOffset = 0.0,
bool keepScrollOffset = true,
String debugLabel,
) : super(
initialScrollOffset: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
debugLabel: debugLabel,
);
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition oldPosition)
return new BidirectionalScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
class BidirectionalScrollPosition extends ScrollPositionWithSingleContext
BidirectionalScrollPosition(
@required ScrollPhysics physics,
@required ScrollContext context,
double initialPixels = 0.0,
bool keepScrollOffset = true,
ScrollPosition oldPosition,
String debugLabel,
this.negativeScroll = false,
) : assert(negativeScroll != null),
super(
physics: physics,
context: context,
initialPixels: initialPixels,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
)
if (oldPosition != null &&
oldPosition.minScrollExtent != null &&
oldPosition.maxScrollExtent != null)
_minScrollExtent = oldPosition.minScrollExtent;
_maxScrollExtent = oldPosition.maxScrollExtent;
final bool negativeScroll;
void _forceNegativePixels(double value)
super.forcePixels(-value);
@override
double get minScrollExtent => _minScrollExtent;
double _minScrollExtent = 0.0;
@override
double get maxScrollExtent => _maxScrollExtent;
double _maxScrollExtent = 0.0;
void setMinMaxExtent(double minExtent, double maxExtent)
_minScrollExtent = minExtent;
_maxScrollExtent = maxExtent;
@override
void saveScrollOffset()
if (!negativeScroll)
super.saveScrollOffset();
@override
void restoreScrollOffset()
if (!negativeScroll)
super.restoreScrollOffset();
以下示例演示了在上下两个方向滚动边界的延迟加载:
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:tiverme/ui/helpwidgets/BidirectionalListView.dart';
class MyHome extends StatefulWidget
@override
_MyHomeState createState() => new _MyHomeState();
class _MyHomeState extends State<MyHome>
BidirectionalScrollController controller;
Map<int, String> items = new Map();
static const double ITEM_HEIGHT = 30;
@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
Widget build(BuildContext context)
List<int> keys = items.keys.toList();
keys.sort();
int negativeItemCount = keys.first;
int itemCount = keys.last;
print("itemCount = " + itemCount.toString());
print("negativeItemCount = " + negativeItemCount.abs().toString());
return new Scaffold(
body: new Scrollbar(
child: new BidirectionalListView.builder(
controller: controller,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index)
return Container(
child: Text(items[index]),
height: ITEM_HEIGHT,
padding: EdgeInsets.all(0),
margin: EdgeInsets.all(0));
,
itemCount: itemCount,
negativeItemCount: negativeItemCount.abs(),
),
),
);
void _rebuild() => setState(() );
double oldScrollPosition = 0.0;
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 * ITEM_HEIGHT - 3 * ITEM_HEIGHT);
double negativeReloadBorder =
(-(negativeItemCount * ITEM_HEIGHT - 3 * ITEM_HEIGHT));
print("pixels = " + controller.position.pixels.toString());
print("itemCount = " + itemCount.toString());
print("negativeItemCount = " + negativeItemCount.toString());
print("minExtent = " + controller.position.minScrollExtent.toString());
print("maxExtent = " + controller.position.maxScrollExtent.toString());
print("positiveReloadBorder = " + positiveReloadBorder.toString());
print("negativeReloadBorder = " + negativeReloadBorder.toString());
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;
try
BidirectionalScrollPosition pos = controller.position;
pos.setMinMaxExtent(
-negativeItemCount * ITEM_HEIGHT, itemCount * ITEM_HEIGHT);
catch (error)
print(error.toString());
if (rebuildNecessary)
_rebuild();
oldScrollPosition = controller.position.pixels;
【讨论】:
我尝试了您的解决方案,但在我的情况下不起作用。我不断收到'minScrollExtent'被调用为null。我需要一个可以滚动到顶部和底部的列表,而不是无限滚动,因为我已经知道我有多少项目,它只是它的月份列表,当前月份总是首先加载。我开始颤抖,我对什么是最好的方法有点迷茫..有什么建议吗? 感谢您的评论!我刚刚复制了您提到的错误。应该使用更新的答案来修复它。以上是关于Flutter ListView 双向延迟加载(上、下)的主要内容,如果未能解决你的问题,请参考以下文章
Flutter ListView优化(滑动不加载,停止滑动加载)
Flutter IndexedWidgetBuilder 与 ListView.builder 与动态项目数?
在 Flutter 中创建一个双向无限 ListView.builder