我写了一篇关于清理 Flutter UI 代码的文章已经快 6 个月

这篇文章提供了一些关于如何组织 Flutter UI 代码以减少混乱和提高可读性的技巧。它仍然是一篇非常受欢迎的文章。然而,那里有一个提示,主张做一些结果证明不是那么好的事情。

为了摆脱 Lisp-y括号地狱,我主张将长构建方法拆分为多个单独的较小方法。下面的代码示例完全是无意义的,但我们会假装这是一个真正有用的片段。

因此,例如,如果我们有一个看起来像这样的小部件:

嵌套构建方法的示例

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('Counter: $_counter'),
        Container(
          child: Column(
            children: [
              Text('Hello'),
              Row(
                children: [
                  Text('there'),
                  Text('world!'),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }
}

查看代码,您可能会觉得嵌套级别有点疯狂。

他们这样做。减少一点缩进级别会很棒。由于小部件可能有点样板,因此想到的第一个解决方案是将嵌套部分拆分为单独的方法。

我们的第一个直觉可能会结束我们看起来像这样的事情:

嵌套构建方法的示例 - 拆分为单独的方法

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _counter = 0;

  Widget _buildNonsenseWidget() {
    return Container(
      child: Column(
        children: [
          Text('Hello'),
          Row(
            children: [
              Text('there'),
              Text('world!'),
            ],
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('Counter: $_counter'),

        // The deeply nesting widget is now refactored into a
        // separate method and we have a cleaner build method. Yay!
        _buildNonsenseWidget(),
      ],
    );
  }
}

问题解决了对吧?是时候收工回家了!

嗯,不完全是。

将小部件拆分为方法的问题

乍一看,将长构建方法拆分为小函数非常有意义。你当然可以看到这种模式在野外经常使用。这也是在我们的代码库中大量使用Reflectly -相信我,我们有一大堆的UI代码。

现在,在我最初的文章发表 6 个月后,我在这里告诉你你不应该这样做。如果你在阅读我的文章后养成了这个坏习惯,你现在可能会生我的气。不客气。这就是朋友们为彼此所做的事情。我非常抱歉。

Wm Leler在上述文章的评论部分首先引起了我的注意。

Wm Leler 的评论解释了如何将小部件拆分为函数是一种性能反模式。

Wm Leler 的评论解释了如何将小部件拆分为函数是一种性能反模式。

对于那些还不知道的人,Wm 是 Flutter 的开发者倡导者。

你们中的一些人阅读了 Wm 的评论,现在会有一个哈哈的时刻。然而,你们中的一些人,包括最初的我,不会。这很好 - 我们将了解这里发生了什么。

那么问题是什么,真的吗?

每当值发生_counter变化时,框架都会调用该build方法。这会触发我们的小部件重建自身。问题是每次更改值_buildNonsenseWidget()都会调用它 - 最终会一遍又一遍地重建小部件树。_counter

白白重建

在这种情况下,没有理由重建那个特定的小部件树。

返回的小部件树_buildNonsenseWidget()本质上是无状态的——我们只需要构建一次。可悲的是,由于widget树是通过_buildNonsenseWidget()方法构建的,每次父widget重建时Flutter框架都会重建它。

本质上,我们在重建不需要重建的东西上浪费了宝贵的 CPU 周期。发生这种情况是因为从框架的角度来看,长屁股构建方法和拆分为多个较小方法的构建方法之间没有区别。请注意,这只是一个简单的例子——这对更复杂的应用程序有更重大的影响。

拆分长构建方法 - 重新审视

这个解决方案相对简单,尽管它会产生几行额外的代码。我们没有将构建方法拆分为更小的方法,而是将它们拆分为小部件 - StatelessWidgets,即。

当我们重构前面的例子时,我们会得到这样的结果:

嵌套构建方法的示例 - 拆分为小部件

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('Counter: $_counter'),

        // The deeply nesting widget is now refactored into a
        // stateless const widget. No more needless rebuilding!
        const _NonsenseWidget(),
      ],
    );
  }
}

class _NonsenseWidget extends StatelessWidget {
  const _NonsenseWidget();

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          Text('Hello'),
          Row(
            children: [
              Text('there'),
              Text('world!'),
            ],
          ),
        ],
      ),
    );
  }
}

虽然代码有点多,但这要好得多。

现在_NonsenseWidget只构建了一次,所有不必要的重建都消失了。父小部件可以多次重建自己,但_NonsenseWidget并不在乎 - 它只构建一次。

(这只是故事的一部分,适用于constStatelessWidgets - 请参阅此答案以了解创建小部件的函数和类之间的区别是什么? - Remi Rousselet 的 StackOverflow 答案。)

将小部件拆分为更小的小部件 - 更复杂的示例

您可能会认为以上是一个非常简单的示例,并不代表真实应用程序的复杂性。

你是对的。我最近更新了开源 inKino 应用程序以遵循本文的建议。例如,我认为这是StatelessWidget在更大的应用程序中将小部件拆分为更小的s 的一个很好的示例

结论

不要将构建方法拆分为多个较小的方法,而是将它们拆分为StatelessWidgets。这样,您就不会为了浪费 CPU 周期而多次重建静态小部件树。当谈到优化 Flutter 应用程序的性能时,这可能是悬而未决的成果之一。

如果你真的更喜欢用方法构建你的小部件树,你可能想看看Remi Rousselet 的一个名为functional_widget的包。它通过使用代码生成减轻了使用方法构建小部件树所带来的问题。