带有刷新、加载更多和删除旧数据功能的列表。

7
我正在尝试构建一个列表,您可以在两侧添加项目,并对其进行限制(因此两侧不是无限的)。在顶部,您将执行刷新操作,它将加载新项目,但您将停留在同一位置(新项目位于当前位置之上)。在底部,您将加载更多内容,但仍然停留在同一位置。 我尝试使用CustomScrollView和两个带有中心的sliver来实现此目的,但我想要做的事情是,在刷新时删除一些旧项,但实际上会跳到新的中心并刷新卡片。这是在顶部的那一点。
CustomScrollView(
                physics: const BouncingScrollPhysics(
                  parent: AlwaysScrollableScrollPhysics(),
                ),
                center: _mainListKey,
                controller: scrollController,
                cacheExtent: 1000,
                slivers: <Widget>[
                  SliverList(
                      delegate: SliverChildBuilderDelegate(
                        (BuildContext context, int index) {
                     return ComplexWidget(item: topItems[index]),
                  },
                  childCount: topItems.count,
                  ),),
                   SliverList(
                      key:  _mainListKey
                      delegate: SliverChildBuilderDelegate(
                        (BuildContext context, int index) {
                     return ComplexWidget(item: bottomItems[index]),
                  },
                  childCount: bottomItems.count,
                  ),),
                ]
              ),

更好的行为描述:
假设您在 Twitter 应用程序中查看一个列表。如果向下滚动,它会加载越来越多、越来越旧的推文。有时,它会在后台加载较新的项目,这些项目将被加载到顶部,而无需让您滚动。最后,您可以返回顶部并下拉以刷新,它将下载新项目(Twitter 上有区别),并将这些项目添加到您的位置之上。
因此,有三个触发器:
1. 向下滚动以带有指示器的方式加载更多内容。 2. 下拉以刷新。 3. 后台自动刷新。
以上这些操作都不应使您更改位置。

1
当添加新项目时,您需要保留当前位置,对吗? - Yashraj
1
@Yashraj 是的,上下都有。 - Dominik Šimoník
1
这个线程对你有帮助吗?https://dev59.com/PVUM5IYBdhLWcg3wOeJY - Karolina Hagegård
1
@KarolinaHagegård 我的问题不在于滚动,而是在于更新列表中的项目数量。当我向列表添加新项目时,我可以同时执行scrollTo和jumpTo,但这似乎是一种hack方法,并且并非始终有效,特别是当你的项目高度不是固定的时候。 - Dominik Šimoník
1
但是你说你希望该项保持在屏幕上的同一位置,但它没有?这不是滚动的问题吗?例如,如此答案建议的ScrollablePositionedList()怎么样?https://dev59.com/PVUM5IYBdhLWcg3wOeJY#58924218 - Karolina Hagegård
@KarolinaHagegård 是的。请查看yellowgray的回答,我们可能没有理解预期的行为。他很接近,但是在刷新时,他确实有额外的滚动到最新的内容,而我不想要这个。我尝试了可滚动位置列表,并在刷新时跳转到我开始刷新的索引位置,但它并不可靠。 - Dominik Šimoník
2个回答

1
由于解决方案非常不同,需要使用包scrollable_positioned_list,因此我在此添加另一个答案。
第一个要求是您假设子元素的高度是灵活的。因此,我建议使用scrollable_positioned_list,然后您可以滚动到索引而不知道具体的滚动位置。
第二个要求是您想从列表开头加载新项目并保持位置。仅使用listview是不可能的,因为listview始终从索引0计算项目数。因此,您需要外部函数,例如jumpToposition.correctPixels或其他可以使插入项从索引0开始的位置正确的函数。
不幸的是,scrollable_positioned_list只能使用jumpTo来更改位置:它会中断滚动。如果您认为这是个问题,您可能需要实现自定义“可滚动定位列表”,该列表支持一些函数,如correctPixels,这不会中断用户的滚动。
示例代码
我将列表移入小部件,因为它需要完全控制每个项目。 MyPostService
int countTop = -1;
int countBottom = 21;
class MyPostService {
  static Future<List<String>> fetchNew() async {
    await Future.delayed(const Duration(seconds: 1));
    final result = List.generate(5, (index) => '*post ${countTop + index - 4}');
    countTop -= 5;
    return result;
  }

  static Future<List<String>> loadMore() async {
    await Future.delayed(const Duration(seconds: 1));
    final result = List.generate(5, (index) => '*post ${countBottom + index}');
    countBottom += 5;
    return result;
  }
}

TestPositionedList

class TestPositionedList extends StatefulWidget {
  const TestPositionedList({
    required this.initialList,
    Key? key,
  }) : super(key: key);

  final List<String> initialList;

  @override
  State<TestPositionedList> createState() => _TestPositionedListState();
}

class _TestPositionedListState extends State<TestPositionedList> {
  final itemScrollController = ItemScrollController();
  final itemPositionsListener = ItemPositionsListener.create();

  /// list items
  List<String?> posts = [];

  /// Current index and offset
  int currentIndex = 0;
  double itemLeadingEdge = 0.0;

  /// Show loading indicator
  bool isLoadingMore = false;

  @override
  void initState() {
    posts = widget.initialList;
    itemPositionsListener.itemPositions.addListener(() async {
      if (posts.isNotEmpty) {
        /// Save current index and offset
        final firstItem = itemPositionsListener.itemPositions.value.first;
        currentIndex = firstItem.index;
        itemLeadingEdge = firstItem.itemLeadingEdge;

        /// load more
        final lastItem = itemPositionsListener.itemPositions.value.last;
        if (lastItem.itemTrailingEdge < 1) {
          await _onLoadMore();
        }
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final finalList = [
      ...posts,
      if (isLoadingMore) null,
    ];

    return RefreshIndicator(
      onRefresh: _onRefresh,
      child: ScrollablePositionedList.builder(
        itemScrollController: itemScrollController,
        itemPositionsListener: itemPositionsListener,
        itemCount: finalList.length,
        itemBuilder: (_, index) {
          final post = finalList[index];
          if (post != null) {
            return ListTile(title: Text(post));
          } else {
            return _loadingIndicator();
          }
        },
      ),
    );
  }

  Future _onRefresh() async {
    final newPosts = await MyPostService.fetchNew();
    final firstItem = itemPositionsListener.itemPositions.value.first;
    setState(() {
      posts.insertAll(0, newPosts);
      itemScrollController.jumpTo(
        index: firstItem.index + newPosts.length,
        alignment: firstItem.itemLeadingEdge,
      );
    });
  }

  Future _onLoadMore() async {
    if (!isLoadingMore) {
      setState(() => isLoadingMore = true);
      final morePosts = await MyPostService.loadMore();
      setState(() {
        posts.addAll(morePosts);
        isLoadingMore = false;
      });
    }
  }

  Widget _loadingIndicator() {
    return const Center(
      child: Padding(
        padding: EdgeInsets.symmetric(vertical: 20),
        child: CircularProgressIndicator(),
      ),
    );
  }
}

1
我已经成功将可滚动定位列表的fork进行了处理,不需要通过itemScrollControlled跳转。我在可滚动定位列表中添加了两个参数。
  final bool keepPosition;
  final String Function(int index)? onItemKey;

在构建时,我已经添加了以下内容。
    if (widget.itemCount > 0 && widget.onItemKey != null) {
      _lastTargetKey = widget.onItemKey!(primary.target);
    } else {
      _lastTargetKey = null;
    }

索引的额外搜索

  String? _lastTargetKey;
  int? _getIndexOfKey() {
    if (widget.onItemKey != null) {
      int? index;
      for (var i = 0; i < widget.itemCount; i++) {
        if (widget.onItemKey!(i) == _lastTargetKey) {
          index = i;
          break;
        }
      }
      return index;
    }
  }

并添加到didUpdateWidget中

    if (widget.keepPosition) {
      if (_lastTargetKey != null) {
        var foundIndex = _getIndexOfKey();
        if (foundIndex != null && foundIndex > primary.target) {
          primary.target = foundIndex;
        }
      }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接