【问题标题】:How to deal with unwanted widget build?如何处理不需要的小部件构建?
【发布时间】:2019-02-14 09:05:19
【问题描述】:

由于各种原因,有时我的小部件的build方法会被再次调用。

我知道这是因为父母更新了。但这会导致不良影响。 导致问题的典型情况是以这种方式使用FutureBuilder

@override
Widget build(BuildContext context) {
  return FutureBuilder(
    future: httpCall(),
    builder: (context, snapshot) {
      // create some layout here
    },
  );
}

在本例中,如果再次调用 build 方法,则会触发另一个 HTTP 请求。这是不受欢迎的。

考虑到这一点,如何处理不需要的构建?有什么方法可以阻止构建调用?

【问题讨论】:

  • provider documentation 中,您在此处链接说“请参阅此 stackoverflow 答案,其中详细解释了为什么不希望使用 .value 构造函数来创建值。”但是,您没有在此处或您的答案中提及值构造函数。您的意思是链接到其他地方吗?
  • @Suragch 这是正确的链接。该问题并非特定于提供者,“.value”构造函数的问题与此处描述的相同。即用 SomeProvider.value 替换 FutureBuilder
  • 我建议直接在文档中解释不良副作用(第一选择)或在此处添加更多解释(第二选择)。我不知道我是否代表普通 Provider 用户,但是当我来到这里时,我仍然不明白使用 .value 和不需要的小部件构建或 build 方法之间的关系需要纯.
  • @Suragch 我还发现提供程序文档的that part 非常混乱。更清晰的解释可以在Flutter by Example找到。

标签: flutter dart flutter-layout flutter-widget unbound


【解决方案1】:

build 方法的设计方式应该是纯/没有副作用。这是因为许多外部因素都可以触发新的小部件构建,例如:

  • 路由弹出/推送
  • 屏幕调整大小,通常是由于键盘外观或方向改变
  • 父小部件重新创建了它的子小部件
  • 小部件所依赖的 InheritedWidget(Class.of(context) 模式)更改

这意味着build 方法不应触发 http 调用或修改任何状态


这与问题有什么关系?

您面临的问题是您的构建方法有副作用/不纯,使得无关的构建调用很麻烦。

您应该使您的构建方法纯粹,而不是阻止构建调用,以便可以随时调用它而不会受到影响。

在您的示例中,您需要将小部件转换为 StatefulWidget,然后将该 HTTP 调用提取到您的 StateinitState

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  Future<int> future;

  @override
  void initState() {
    future = Future.value(42);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: future,
      builder: (context, snapshot) {
        // create some layout here
      },
    );
  }
}

我已经知道了。我来到这里是因为我真的想要优化重建

也可以使小部件能够重建,而无需强制其子级也进行构建。

当小部件的实例保持不变时; Flutter 故意不会重建孩子。这意味着您可以缓存部分小部件树以防止不必要的重建。

最简单的方法是使用 dart const 构造函数:

@override
Widget build(BuildContext context) {
  return const DecoratedBox(
    decoration: BoxDecoration(),
    child: Text("Hello World"),
  );
}

感谢const 关键字,即使调用了数百次,DecoratedBox 的实例也将保持不变。

但您可以手动获得相同的结果:

@override
Widget build(BuildContext context) {
  final subtree = MyWidget(
    child: Text("Hello World")
  );

  return StreamBuilder<String>(
    stream: stream,
    initialData: "Foo",
    builder: (context, snapshot) {
      return Column(
        children: <Widget>[
          Text(snapshot.data),
          subtree,
        ],
      );
    },
  );
}

在此示例中,当 StreamBuilder 收到新值通知时,subtree 不会重建,即使 StreamBuilder/Column 这样做。 这是因为,由于关闭,MyWidget 的实例没有改变。

这种模式在动画中被大量使用。典型用途是AnimatedBuilder 和所有转换,例如AlignTransition

您也可以将subtree 存储到您班级的字段中,但不推荐使用,因为它会破坏热重载功能。

【讨论】:

  • 您能解释一下为什么在类字段中存储subtree 会破坏热重载吗?
  • StreamBuilder 遇到的一个问题是,当键盘出现时,屏幕会发生变化,因此必须重建路线。所以StreamBuilder 被重建并创建了一个新的StreamBuilder 并订阅stream。当StreamBuilder 订阅stream 时,snapshot.connectionState 变为ConnectionState.waiting,这使我的代码返回CircularProgressIndicator,然后snapshot.connectionState 在有数据时发生变化,我的代码将返回一个不同的小部件,这使得屏幕闪烁着不同的东西。
  • 我决定创建一个StatefulWidget,订阅initState() 上的stream 并将currentWidgetsetState() 设置为stream 发送新数据,通过currentWidgetbuild() 方法。有没有更好的解决方案?
  • 我有点困惑。您正在回答自己的问题,但从内容来看,它看起来不像。
  • 呃,说构建不应该调用 HTTP 方法完全违背了 FutureBuilder 的非常实际的例子。
【解决方案2】:

您可以使用这些方式防止不需要的构建调用

  1. 为 UI 的各个小部分创建子 Statefull 类

  2. 使用Provider 库,因此使用它可以阻止不需要的构建方法调用

    在以下这些情况下构建方法调用

  • 致电initState
  • 致电didUpdateWidget
  • setState() 被调用时。
  • 当键盘打开时
  • 屏幕方向更改时
  • 如果父小部件已构建,则子小部件也将重建

【讨论】:

  • 第一点干扰了最后一个“为 UI 的单个小部分创建子 Statefull 类”与“父小部件已构建,然后子小部件也重建”
  • 不,让我举个例子,第一个例子是如果你有注册表单屏幕并创建小的子 ui 来获取 BDay,所以当你重建 BDay 小部件时,整个注册表单屏幕不会重建但是如果你重建父屏幕然后整个孩子也正在重建
  • 如果有人仍然想知道,@Sanjayrajsinh 意味着您应该创建小的单独的有状态小部件,因为在这些小部件中更新状态不会影响父级。如果你有例如每个 setState() 都会更新所有内容的巨大小部件
【解决方案3】:

Flutter 还有ValueListenableBuilder&lt;T&gt; class 。它允许您仅重建某些必要的小部件并跳过昂贵的小部件。

你可以在这里查看文档ValueListenableBuilder flutter docs
或者只是下面的示例代码:

  return Scaffold(
  appBar: AppBar(
    title: Text(widget.title)
  ),
  body: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text('You have pushed the button this many times:'),
        ValueListenableBuilder(
          builder: (BuildContext context, int value, Widget child) {
            // This builder will only get called when the _counter
            // is updated.
            return Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                Text('$value'),
                child,
              ],
            );
          },
          valueListenable: _counter,
          // The child parameter is most helpful if the child is
          // expensive to build and does not depend on the value from
          // the notifier.
          child: goodJob,
        )
      ],
    ),
  ),
  floatingActionButton: FloatingActionButton(
    child: Icon(Icons.plus_one),
    onPressed: () => _counter.value += 1,
  ),
);

【讨论】:

    【解决方案4】:

    避免通常由调用setState() 以仅更新特定小部件而不刷新整个页面而引起的不需要的重新构建的最简单方法之一是剪切该部分您的代码并将其作为独立的 Widget 包装在另一个 Stateful 类中。
    例如在下面的代码中,父页面的Build方法通过按下FAB按钮反复调用:

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(TestApp());
    }
    
    class TestApp extends StatefulWidget {
    
      @override
      _TestAppState createState() => _TestAppState();
    }
    
    class _TestAppState extends State<TestApp> {
    
      int c = 0;
    
      @override
      Widget build(BuildContext context) {
    
        print('build is called');
    
        return MaterialApp(home: Scaffold(
          appBar: AppBar(
            title: Text('my test app'),
          ),
          body: Center(child:Text('this is a test page')),
          floatingActionButton: FloatingActionButton(
            onPressed: (){
              setState(() {
                c++;
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.wb_incandescent_outlined, color: (c % 2) == 0 ? Colors.white : Colors.black)
          )
        ));
      }
    }
    
    

    但是如果将FloatingActionButton小部件分离到另一个类中,有自己的生命周期,setState()方法不会导致父类Build方法重新运行:

    import 'package:flutter/material.dart';
    import 'package:flutter_app_mohsen/widgets/my_widget.dart';
    
    void main() {
      runApp(TestApp());
    }
    
    class TestApp extends StatefulWidget {
    
      @override
      _TestAppState createState() => _TestAppState();
    }
    
    class _TestAppState extends State<TestApp> {
    
      int c = 0;
    
      @override
      Widget build(BuildContext context) {
    
        print('build is called');
    
        return MaterialApp(home: Scaffold(
          appBar: AppBar(
            title: Text('my test app'),
          ),
          body: Center(child:Text('this is a test page')),
          floatingActionButton: MyWidget(number: c)
        ));
      }
    }
    
    

    和 MyWidget 类:

    import 'package:flutter/material.dart';
    
    class MyWidget extends StatefulWidget {
    
      int number;
      MyWidget({this.number});
    
      @override
      _MyWidgetState createState() => _MyWidgetState();
    }
    
    class _MyWidgetState extends State<MyWidget> {
    
      @override
      Widget build(BuildContext context) {
        return FloatingActionButton(
            onPressed: (){
              setState(() {
                widget.number++;
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.wb_incandescent_outlined, color: (widget.number % 2) == 0 ? Colors.white : Colors.black)
        );
      }
    }
    
    

    【讨论】:

      【解决方案5】:

      我只是想分享我主要由于上下文而构建不需要的小部件的经验,但我找到了一种非常有效的方法

      • 路由弹出/推送

      所以你需要使用 Navigator.pushReplacement() 以便前一页的上下文与即将到来的页面没有关系

      1. 使用 Navigator.pushReplacement() 从第一页导航到第二页
      2. 在第二页我们需要再次使用 Navigator.pushReplacement() 在 appBar 我们添加 -
        leading: IconButton(
                icon: Icon(Icons.arrow_back),
                onPressed: () {
                  Navigator.pushReplacement(
                    context,
                    RightToLeft(page: MyHomePage()),
                  );
                },
              )
        

      通过这种方式我们可以优化我们的应用程序

      【讨论】:

        猜你喜欢
        • 2017-02-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-07-06
        相关资源
        最近更新 更多