简体   繁体   English

Flutter ListView 在每次 setState() 调用时跳转到顶部

[英]Flutter ListView jumps to top on each setState() call

I have a ListView of customers where the user is able to check/uncheck each customer to mark them as a 'favourite'.我有一个客户列表视图,用户可以在其中选中/取消选中每个客户以将他们标记为“收藏夹”。 However, each time a ListItem is tapped, the ListView jumps back to the top of the screen.但是,每次点击 ListItem 时,ListView 都会跳回到屏幕顶部。

How do I prevent this from happening?我如何防止这种情况发生? I only want the Checkbox to refresh, not the entire screen, and to prevent the screen from jumping to the top each time.我只希望 Checkbox 刷新,而不是整个屏幕,并防止屏幕每次都跳到顶部。

@override
Widget build(BuildContext context) {
  customers = getAllCustomers();

  return Scaffold(
     appBar: _getAppBar(),
     body: customers != null
          ? FutureBuilder(
              future: getFavouriteCustomers(), // contains IDs of favourite customers
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.done) {
                  if (snapshot.hasData) {
                    List<String> favouriteCustomersList = snapshot.data;

                    return ListView.builder(
                        itemCount: customers?.length ?? 0,
                        itemBuilder: (BuildContext context, int index) {
                          customer c = customers?.elementAt(index);

                          if (favouriteCustomersList.contains(c.id)) {
                            c.isSelected = true;
                          }

                          return ListTile(
                            title: Text(c.name),
                            trailing: Checkbox(
                                value: c.isFavourite,
                                onChanged: (newValue) {}),
                            onTap: () {
                              if (c.isSelected) {
                                setState(() {
                                  c.setFavourite(false);
                                });
                              } else {
                                setState(() {
                                  c.setFavourite(true);
                                }
                              }
                            },
                          );
                        });
                  }
                } else {
                  return CircularProgressIndicator();
                }
              })
          : Center(
              child: CircularProgressIndicator(),
            );
);

} }

  1. Ensure you are doing this in a StatefulWidget.确保您在 StatefulWidget 中执行此操作。

  2. Create a ScrollController as field in the state class (not the widget class):在状态类(不是小部件类)中创建一个ScrollController作为字段:

  _controller = ScrollController(keepScrollOffset: true);

This way the controller will be preserved in the state and not recreated on rebuilds.这样,控制器将保留在状态中,而不是在重建时重新创建。

  1. Pass it to the builder as将它传递给构建器作为
  controller: _controller,
  1. Remember to dispose the controller when the state is destroyed:记得在状态销毁时处置控制器:
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  1. As Benno mentioned, ensure that all the way up the widget tree there are no widgets with keys being recreated on rebuilds: How to prevent Flutter app from scrolling to top after calling setState?正如 Benno 所提到的,确保在小部件树上一直没有小部件在重建时重新创建键:如何在调用 setState 后防止 Flutter 应用程序滚动到顶部?

Not sure if you already found the answer, but I generally solve the problem by splitting your build methods by using two StatefulWidgets.不确定您是否已经找到答案,但我通常通过使用两个 StatefulWidgets 拆分构建方法来解决问题。 What was going on here was, every time the setState() is called, the whole widget is rebuilt because the state was getting changed for your whole State class.这里发生的事情是,每次调用setState() ,都会重建整个小部件,因为整个 State 类的状态都发生了变化。 That means the build method was getting called forcing scaffold to be recreated along with all the widget tree in it.这意味着 build 方法被调用,强制重新创建脚手架及其中的所有小部件树。 Since the widget was rebuilt, it will assume it's original position which was offset 0. I would suggest to create another stateful widget for returning ListTile object.由于小部件已重建,它将假定它的原始位置偏移量为 0。我建议创建另一个有状态小部件来返回ListTile对象。 This will break the build method and action performed on each ListTile is performed within it's own build method.这将破坏构建方法,并且在每个ListTile上执行的操作是在它自己的build方法中执行的。 This won't force your Scaffold to be recreated.这不会强制您重新创建 Scaffold。

I don't have your full code so can't suggest complete working solution but something like below may help.我没有您的完整代码,因此无法建议完整的工作解决方案,但以下内容可能会有所帮助。


class SomeStatefulWidget extends StatefulWidget {

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

class SomeStatefulWidgetState extends State<SomeStatefulWidget> {
@override
Widget build(BuildContext context) {
  customers = getAllCustomers();

  return Scaffold(
     appBar: _getAppBar(),
     body: customers != null
          ? FutureBuilder(
              future: getFavouriteCustomers(), // contains IDs of favourite customers
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.done) {
                  if (snapshot.hasData) {
                    List<String> favouriteCustomersList = snapshot.data;

                    return ListView.builder(
                        itemCount: customers?.length ?? 0,
                        itemBuilder: (BuildContext context, int index) {
                          customer c = customers?.elementAt(index);

                          if (favouriteCustomersList.contains(c.id)) {
                            c.isSelected = true;
                          }

                          return MyListTile(c);

                        });
                  }
                } else {
                  return CircularProgressIndicator();
                }
              })
          : Center(
              child: CircularProgressIndicator(),
            );
);
}


class MyListTile extends StatefulWidget {

  customer c;

  @override
  _MyListTileState createState(this.c) => _MyListTileState();

}

class _MyListTileState extends State<MyListTile> {

  @override
  Widget build(BuildContext context) {

    var c = widget.c;

    return ListTile(
      title: Text(c.name),
      trailing: Checkbox(
          value: c.isFavourite,
          onChanged: (newValue) {}),
      onTap: () {
        if (c.isSelected) {
          setState(() {
            c.setFavourite(false);
          });
        } else {
          setState(() {
            c.setFavourite(true);
          }
        }
      },
    );
  }
}

By splitting the widget in to two, setState() will now only build the MyListTile object by calling build method of MyListTileState class.通过将小部件一分为二, setState()现在将仅通过调用MyListTileState类的build方法来构建MyListTile对象。 You effectively wrapped ListTile in to your own stateful class named MyListTile .您有效地将ListTile包装到您自己的名为MyListTile的有状态类中。

I hope this works for you.我希望这对你有用。 As said, I don't have your full code hence made some wrapper classes to make this example clear.如前所述,我没有你的完整代码,因此制作了一些包装类来使这个例子清晰。 Also, I haven't compiled this code and typed everything here so there may be some compile time error.此外,我还没有编译这段代码并在此处输入所有内容,因此可能存在一些编译时错误。 But this explains the concept, reason of jumping to top and solution.但这解释了跳到顶部的概念、原因和解决方案。

Instead of using setState() to update the UI, you can try wrapping the listTile inside a StreamBuilder and provide it with a stream which sends data every time the checkbox is tapped.您可以尝试将 listTile 包装在 StreamBuilder 中,而不是使用 setState() 来更新 UI,并为其提供每次点击复选框时都会发送数据的流。 Once StreamBuilder receives data from the stream, it will update just the cell, and not the whole list, and eventually you will be able to avoid the top-scrolling of list.一旦 StreamBuilder 从流中接收到数据,它将只更新单元格,而不是整个列表,最终您将能够避免列表的顶部滚动。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM