Flutter中ListView跳转到顶部

13
我正在为我的应用程序创建一个新闻动态,当我向下滚动时,数据从Firestore中获取。但只要我向上滚动,列表视图就会立即跳到顶部,跳过所有其他项目。如果我使用静态数据加载它,则列表视图正常工作,但当我从Firestore拉取数据时,问题重新出现。我不确定是什么原因导致了这种行为。

演示异常滚动行为的视频

这是我的代码:

return StreamBuilder(
            stream: Firestore.instance.collection(Constants.NEWS_FEED_NODE)
                .document("m9yyOjsPxV06BrxUtHdp").
            collection(Constants.POSTS_NODE).orderBy("timestamp",descending: true).snapshots(),
            builder: (BuildContext context, AsyncSnapshot snapshot) {
              if (!snapshot.hasData)
                return Center(child: CircularProgressIndicator(),);

              if (snapshot.data.documents.length == 0)
                return Container();
              //after getting the post ids, we then make a call to FireStore again
              //to retrieve the details of the individual posts
              return ListView.builder(
                  itemCount: snapshot.data.documents.length,
                  itemBuilder: (_, int index) {
                    return FeedItem2(feed: Feed.fireStore(
                        snapshot: snapshot.data.documents[index]),);
                  });
            },
          );

你能否将 return FeedItem2() 替换为 return Container(padding: EdgeInsets.all(50.0), child: Text("test")); 并查看问题是否消失?如果是这样,可能指向 FeedItem2 widget 是问题所在,这种情况下我们需要查看该 widget 的代码。我假设该 widget 正在执行某些动态操作。 - Ashton Thomas
1
为了好玩,尝试将 cacheExtent: 10000.0 传递给 ListView 工厂构造函数。这可能有助于我们确定是否存在问题,即当您滚动到当前可见内容时,ListView 如何处理小部件的释放和重新初始化。 - Ashton Thomas
也许可以确保FeedItem2始终具有相同的高度(在第一次渲染和任何后续操作中,例如加载图像)。因为在列表视图中动态更改元素的高度可能会导致一些屏幕外的元素初始化,从而将它们推出视图以被处理。我无法从视频中确定是否存在这种情况。顺便说一句。 - Ashton Thomas
对我来说,解决方案是将cacheExtent(任何合理的像素数)与shrinkWrap:false相结合。 - tomas
@AshtonThomas,cacheExtent 对解决这个问题确实有帮助,但它也需要相当大的内存。难道没有其他方法来解决这个问题吗?这似乎是 Flutter 应用程序的一个重大问题。 - Dennis Ashford
4个回答

8

问题的原因:

ListView和一般的ScrollView会自动清除当前不在屏幕上显示的子组件。当我们重新滚动回该子组件时,该孩子将被从头开始再次初始化。但是在这种情况下,我们的子组件是一个FutureBuilder,重新初始化它会再次创建一个进度指示器,但只有短暂的一部分时间,然后再次创建页面。这会使滚动机制出现混乱,让我们不确定的被扔来扔去。

解决方案

解决此问题的方法之一是确保进度指示器具有与页面完全相同的大小,但在大多数情况下,这并不太实用。所以我们将采取一种效率较低的方法,但可以解决我们的问题;我们将防止ListView清除其子项。为此,我们需要使用AutomaticKeepAliveClientMixin将每个子项即每个FutureBuilder进行包装。这个混合类使子组件请求其父组件在离开屏幕时保持其活动状态,这将解决我们的问题。所以:

  1. 使用KeepAliveFutureBuilder替换你的代码中的FutureBuilder
  2. 创建KeepAliveFutureBuilder小部件:
class KeepAliveFutureBuilder extends StatefulWidget {

  final Future future;
  final AsyncWidgetBuilder builder;

  KeepAliveFutureBuilder({
    this.future,
    this.builder
  });

  @override
  _KeepAliveFutureBuilderState createState() => _KeepAliveFutureBuilderState();
}

class _KeepAliveFutureBuilderState extends State<KeepAliveFutureBuilder> with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: widget.future,
      builder: widget.builder,
    );
  }

  @override
  bool get wantKeepAlive => true;
}
  • 这个小部件只是未来构造器的包装器。它是StatefulWidget,其State扩展了带有AutomaticKeepAliveClientMixin的State类。
  • 它实现了wantKeppAlive getter,并使其简单地返回true,以表示我们希望此子项保持活动状态,从而通知给ListView。

1
但是对于非常长的列表(想象一下你曾经收到的所有电子邮件的列表),这并不是真正实用的,是吗?屏幕外视图被处理掉是有原因的。 - Ridcully
同意。在我看来,最好的方法是存储图像大小并使用此信息来避免问题。 - Dmitriy Blokhin
+1 这里解决了我的问题。我有一个小列表,其中包含大小迥异的小部件。使用 KeepAliveFutureBuilder,它们只会在第一次加载(如果它们被滚动到)时加载,并保留在内存中。当整个页面和/或数据重新加载时,它们会像往常一样重新加载。 - SavioKingdom

6

我也遇到了使用Flutter 1.17.3时相同的问题。根据@Ashton-Thomas在这里的评论:Flutter ListView跳到顶部,我添加了以下内容:

cacheExtent: estimatedCellHeight * numberOfItems,

cacheExtent传递给我的ListView构造函数,问题就解决了。这也极大地提高了整个列表的响应速度。

注意:我的列表中仅有大约50个项目,因此cacheExtent并不是非常高。如果您有数百/数千个项目,则可能需要进行调整。


5
这个问题有两种解决方案: 解决方案1: (如果ListView的高度是动态的)
  1. 使用SingleChildScrollView包装ListView
  2. 将ListView的physics属性设置为 const NeverScrollableScrollPhysics()
  3. 设置shrinkWrap: true以避免渲染问题
或者 解决方案2: (如果ListView的高度已知或可以预测)
  • cacheExtent:设置为ListView的高度 - 它是一个double值。
  • 例如 - 如果ListView中有10个项目,每个项目的高度为20.0,则设置cacheExtent: 200.0

添加NeverScrollableScrollPhysics()可以避免滚动,这是我认为的反模式! - emanuel sanga
当您在另一个ScrollView内使用ScrollView时,应为子项使用NeverScrollableScrollPhysics()。在上述情况中,我们不确定ListView元素的高度时,应使用上述解决方案将其包装在SingleChildScrollView()中。这将解决此问题,因为它会取父容器的ScrollController并忽略ListView的ScrollController。这样做不会使滚动每次向上滚动时都自动回到顶部。 - Sankar Arumugam
我明白了,因为它在SingleCh...里面,对吧?那为什么不把一个Column放在SingleChi...里面呢?帮我理解一下@SankarArumugam。 - emanuel sanga
@emanuel 你可以将列放在SingleChildScrollView中。但是这里的问题是在使用ListView时修复问题。所以它是这样完成的。 - Sankar Arumugam
Anhaa,谢谢你的帮助。 - emanuel sanga
2
这个解决方案对我的情况非常有效,谢谢! - sebdoy

1
只需将您的最顶层小部件包装在SingleChildScrollView()中即可。

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