Flutter:如何在 Provider 更改时防止整个列表重建

huangapple go评论90阅读模式
英文:

Flutter: How to prevent entire list rebuilding when provider changes

问题

我有一个使用简单Provider的ListView.builder,每个项目都需要异步加载数据,因此我正在使用FutureBuilder。

当我向列表添加一个项目时,整个列表都会重新构建。这是有道理的,因为它包装在Consumer中,但不幸的是,这意味着现有项目需要重新运行Future,尽管这些项目实际上没有改变。

有没有办法只重新构建需要构建的ListView项目(即新项目)?

英文:

I have a ListView.builder using a Consumer of a simple Provider.
Each of the items needs to load something asynchronously so I'm using FutureBuilder.

When I add an item to the list, the entire list is rebuilt. This makes sense because it's wrapped in the Consumer, but unfortunately it means the existing items need to re-run the future despite those items not actually changing.

class ListWithButton extends StatelessWidget {
  const ListWithButton({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<MyProvider>(
      builder: (_, provider, __) => Column(
        children: [
          ElevatedButton(
            onPressed: () {
              provider.addItem();
            },
            child: const Text("Add Item"),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: provider.items.length,
              itemBuilder: (_, index) => FutureBuilder<Color>(
                future: provider.loadSomething(index),
                builder: (_, snapshot) => ListTile(
                  leading: Container(
                    width: 30,
                    height: 30,
                    color: snapshot.connectionState == ConnectionState.done
                        ? snapshot.data!
                        : Colors.red,
                  ),
                  title: Text(index.toString()),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class MyProvider extends ChangeNotifier {
  final List<int> _items = [];

  List<int> get items => _items;

  void addItem() {
    _items.add(_items.length);
    notifyListeners();
  }

  Future<Color> loadSomething(int i) async {
    await Future.delayed(Duration(seconds: 1));
    return Colors.green;
  }
}

Is there a way I can only rebuild the items in the ListView which need building (i.e. the new items)?

Flutter:如何在 Provider 更改时防止整个列表重建

答案1

得分: 1

无法完全阻止列表在添加新子项时重新构建其子部件,但可以通过在构建方法之外初始化并存储Futures来阻止Futures被重新初始化,这在FutureBuilder的文档中是推荐的:

必须在之前获取未来,例如在State.initState、State.didUpdateWidget或State.didChangeDependencies期间。在构造FutureBuilder时不能在State.build或StatelessWidget.build方法调用中创建它。如果将未来与FutureBuilder同时创建,那么每当FutureBuilder的父级重新构建时,异步任务都会重新启动。

为此,您只需像这样更改MyProvider

class MyProvider extends ChangeNotifier {
  final List<int> _items = [];
  final Map<int, Future<Color>> _futureColors = {};

  List<int> get items => _items;

  void addItem() {
    _items.add(_items.length);
    notifyListeners();
  }

  Future<Color>? loadSomething(int i) {
    if (!_futureColors.containsKey(i)) {
      _futureColors[i] = Future.delayed(
        const Duration(seconds: 1),
        () => Colors.green,
      );
    }

    return _futureColors[i];
  }
}

在这里,我们只在第一次使用给定索引调用loadSomething时创建新的未来 - 我们将该未来存储在Map中,以后调用loadSomething时,我们返回已为该索引初始化的未来。 (我之所以使用Map而不是List,是因为无法保证该函数将接收有效的List索引)。

还要注意loadSomething不再是异步函数 - 这很重要,因为我们希望未来同步返回给FutureBuilder。如果我们异步返回未来,您可能会注意到在添加新元素时,列表的元素有时仍会闪烁为红色。

英文:

You cannot necessarily prevent the list from rebuilding its child widgets when a new child is added, but you can prevent the Futures from being re-initialized by initializing and storing them outside of the build method, which is recommended in the documentation for FutureBuilder:

> The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.

To do this, you need only change MyProvider, like so:

class MyProvider extends ChangeNotifier {
  final List&lt;int&gt; _items = [];
  final Map&lt;int, Future&lt;Color&gt;&gt; _futureColors = {};

  List&lt;int&gt; get items =&gt; _items;

  void addItem() {
    _items.add(_items.length);
    notifyListeners();
  }

  Future&lt;Color&gt;? loadSomething(int i) {
    if (!_futureColors.containsKey(i)) {
      _futureColors[i] = Future.delayed(
        const Duration(seconds: 1),
        () =&gt; Colors.green,
      );
    }

    return _futureColors[i];
  }
}

Here we only create a new future the first time loadSomething is called with a given index — We store that future in a Map, and on later calls to loadSomething we return the Future that has already been initialized for that index. (The reason I used a Map instead of a List is that there have no guarantees that the function will receive valid List indices).

Note also that loadSomething is no longer an async function — This is important, as we want the future to be returned to the FutureBuilder synchronously. If we return the future asynchronously, you may notice that the elements of the List still flicker red sometimes when you add new elements.

huangapple
  • 本文由 发表于 2023年2月27日 01:15:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/75573704.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定