在 StreamBuilder 中使用 Selector (Provider) 时不必要的小部件重建

Posted

技术标签:

【中文标题】在 StreamBuilder 中使用 Selector (Provider) 时不必要的小部件重建【英文标题】:Unnecessary Widget Rebuilds While Using Selector (Provider) inside StreamBuilder 【发布时间】:2020-04-21 12:19:31 【问题描述】:

我正在使用Selector,它会在 Bloc 中的数据发生更改时重建。哪个很好,但是当数据发生变化时,它会重新加载整个树,而不仅仅是 Selector 中的构建器。

在我的例子中,选择器位于 StreamBuilder 中。我需要这个,因为流连接到 API。所以在流中我正在构建一些小部件,其中之一是选择器。 Selector 重建依赖于 Stream 数据的小部件。

这是我的代码。我不希望 Stream 被一次又一次地调用。还会调用 Stream,因为每次选择器小部件重建时都会调用 build

ma​​in.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_test/data_bloc.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MultiProvider(providers: [
        ChangeNotifierProvider<DataBloc>(
          create: (_) => DataBloc(),
        )
      ], child: ProviderTest()),
    );
  


class ProviderTest extends StatefulWidget 
  @override
  _ProviderTestState createState() => _ProviderTestState();


class _ProviderTestState extends State<ProviderTest> 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Column(
        children: <Widget>[
          Text("Outside Stream Builder"),
          StreamBuilder(
            stream: Provider.of<DataBloc>(context).getString(),
            builder: (_, AsyncSnapshot<String> snapshot) 
              if (snapshot.hasData) 
                return Column(
                  children: <Widget>[
                    Text("Widget Generated by Stream Data"),
                    Text("Data From Strem : " + snapshot.data),
                    RaisedButton(
                        child: Text("Reload Select"),
                        onPressed: () 
                          Provider.of<DataBloc>(context, listen: false).changeValue(5);
                        ),
                    Selector<DataBloc, int>(
                        selector: (_, val) =>
                            Provider.of<DataBloc>(context, listen: false).val,
                        builder: (_, val, __) 
                          return Container(
                            child: Text(val.toString()),
                          );
                        ),
                  ],
                );
              

              return Container();
            ,
          )
        ],
      ),
    );
  

bloc.dart

import 'package:flutter/foundation.dart';

class DataBloc with ChangeNotifier 

  int _willChange = 0;

  int get val => _willChange;

  void changeValue(int val)
    _willChange++;
    notifyListeners();
  

  Stream<String> getString() 
    print("Stream Called");
    return Stream.fromIterable(["one", "two", "three"]);
  


此外,如果我删除 StreamBuilder,那么 Selector 的行为就像它假设的那样。为什么在这种情况下 StreamBuilder 会重建?有没有办法防止这种情况发生?

【问题讨论】:

您是否尝试过抽象出RaisedButtononPressed 函数(例如在某处引用它)?这样,不会在每次 Stream 强制重建时评估回调函数(因此,流更改) 您可以在您的 initState 上为您的 Stream 创建一个侦听器,该侦听器更新一个保留最新版本数据的变量,然后使用该变量填充您的小部件。这样,Stream 只会在 Widget 第一次加载时被订阅,而不是在重建时订阅。 是的,我尝试了它不起作用的抽象。不过我发现了问题。我已经在下面回答了。这是一个简单的解决方案,解决这个问题后我学到了很多东西。这都是关于 Provider 和 InheritedWidget 如何协同工作的...... 【参考方案1】:

根据您共享的代码,您可以在您的 initState 上为您的 Stream 创建一个侦听器,该侦听器更新一个保存最新版本数据的变量,然后使用该变量填充您的小部件。这样,Stream 只会在第一次加载 Widget 时被订阅,而不是在重建时订阅。我无法直接测试它,因为我没有你的项目。但是请尝试一下。

基于您的代码的代码示例

class ProviderTest extends StatefulWidget 
  @override
  _ProviderTestState createState() => _ProviderTestState();


class _ProviderTestState extends State<ProviderTest> 
  String _snapshotData;

  @override
  void initState() 
    listenToGetString();
    super.initState();
  

  void listenToGetString()
    Provider.of<DataBloc>(context).getString().listen((snapshot)
      setState(() 
        _snapshotData = snapshot.data;
      );
    );
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Column(
        children: <Widget>[
          Text("Outside Stream Builder"),
          Column(
            children: <Widget>[
              Text("Widget Generated by Stream Data"),
              Text("Data From Strem : " + _snapshotData),
              RaisedButton(
                child: Text("Reload Select"),
                onPressed: () 
                  Provider.of<DataBloc>(context, listen: false).changeValue(5);
                
              ),
              Selector<DataBloc, int>(
                selector: (_, val) =>
                  Provider.of<DataBloc>(context, listen: false).val,
                builder: (_, val, __) 
                  return Container(
                    child: Text(val.toString()),
                  );
                
              ),
            ],
          )
        ],
      ),
    );
  

【讨论】:

【参考方案2】:

我在阅读此blog post here 后发现了问题。我缺乏关于提供者库如何工作以及它如何从继承的小部件中完成所有神奇东西的知识

解决这个问题的要点是。 (以上博文中的一个说法)

当 Widget 将自己注册为 Provider 的依赖项时 InheritedWidget,该小部件将在每次变化时重建 “提供的数据”发生(更准确地说,当 notifyListeners() 被调用或当 StreamProvider 的流发出新数据或当 FutureProvider 的未来完成)。

这意味着我正在更改的变量和我正在收听的 Stream 存在于同一个 Bloc 中!那是错误。因此,当我更改 val 并在单个 bloc 中调用 notifyListener() 时,所有内容都会重新加载,这是默认行为。

要解决这个问题,我所要做的就是创建另一个 Bloc 并将 Stream 抽象到那个特定的 bloc(我认为这也是一个很好的做法)。现在notifyListener() 对 Stream 没有影响。

data_bloc.dart

class DataBloc with ChangeNotifier 

  int _willChange = 0;

  String data = "";

  int get val => _willChange;

  void changeValue(int val)
    _willChange++;
    notifyListeners();
  

  Future<String> getData () async 
    return "Data";
  


stream_bloc.dart

import 'package:flutter/foundation.dart';

class StreamBloc with ChangeNotifier 

  Stream<String> getString() 
    print("Stream Called");
    return Stream.fromIterable(["one", "two", "three"]);
  


问题就解决了。现在 Stream 只会在它被调用时被调用,而不是当 data_bloc 中的变量发生变化时调用

【讨论】:

以上是关于在 StreamBuilder 中使用 Selector (Provider) 时不必要的小部件重建的主要内容,如果未能解决你的问题,请参考以下文章

在 StreamBuilder 中使用 SnackBar 的奇特方式是啥?

在 Streambuilder 中的集合上使用 where() 不断返回 null

在 Flutter 中使用 streamBuilder 时条件不起作用

如何在 Flutter 的 StreamBuilder 中使用 Future [重复]

FutureBuilder 与 Streambuilder

使用streambuilder颤动firestore分页