likes
comments
collection
share

Widget 中的 State 解析

作者站长头像
站长
· 阅读数 28

StatefulWidget 应对有交互、需要动态变化视觉效果的场景

StatelessWidget 则用于处理静态的、无状态的视图展示

那么,StatelessWidget 是否有存在的必要?StatefulWidget 是否是 Flutter 中的万金油?

UI 编程范式

在 Flutter 中,如何调整一个控件(Widget)的展示样式,即 UI 编程范式。

原生系统(Android、iOS)或原生 JavaScript 开发中,视图开发是命令式的,需要精确地告诉操作系统或浏览器用何种方式去做事情。比如,如果想要变更界面的某个文案,则需要找到具体的文本控件并调用它的控件方法命令,才能完成文字变更。

下述代码分别展示在 Android、iOS 及原生 Javascript 中,如何将一个文本控件的展示文案更改为 Hello World:

// Android 设置某文本控件展示文案为 Hello World
TextView textView = (TextView) findViewById(R.id.txt);
textView.setText("Hello World");

// iOS 设置某文本控件展示文案为 Hello World
UILabel *label = (UILabel *)[self.view viewWithTag:1234];
label.text = @"Hello World";

// 原生 JavaScript 设置某文本控件展示文案为 Hello World
document.querySelector("#demo").innerHTML = "Hello World!";

与此不同的是,Flutter 的视图开发是声明式的,其核心设计思想就是将视图和数据分离,这与 React 的设计思路完全一致

Flutter 要实现同样的需求,则要麻烦点:除了设计好 Widget 布局方案之外,还需要提前维护一套文案数据集,并为需要变化的 Widget 绑定集中的数据,使 Widget 根据这个数据集完成渲染。

但是,当需要变更界面的文案时,只要改变数据集中的文案数据,并通知 Flutter 框架触发 Widget 的重新渲染即可。比起命令式的视图开发方式需要挨个设置不同组建(Widget)的视觉属性,这种方式要便捷得多。

总结来说,命令式编程强调精确控制过程细节,而声明式编程强调通过意图输出结果整体。对应到 Flutter 中,意图是绑定来组件状态的 State,结果则是重新渲染后的组件。在 Widget 的声明周期内,应用到 State 中的任何更改都将强制 Widget 重新构建。

其中,对于组件完成创建后就无需变更的场景,状态的绑定是可选项。这里的“可选”就区分出了 Widget 的两种类型,即:StatelessWidget 不带绑定状态,而 StatefulWidget 带绑定状态。当所要构建的用户界面不随任何状态信息的变化而变化时,需要选择使用 StatelessWidget,反之则选用 StatefulWidget。前者一般用于静态内容的展示,而后者则用于存在交互反馈的内容呈现中。

StatelessWidget

在 Flutter 中,Widget 采用由父到子、自顶向下的方式进行构建,父 Widget 控制着 Widget 的显示样式,其样式配置由父 Widget 在构建时提供。

用如上方式构建出的 Widget,有些(比如 Text、Container、Row、Column 等)在创建时,除了这些配置参数之外不依赖于任何其他信息,换句话说,它们一旦创建成功就不再关心、也不响应任何数据变化进行重绘。在 Flutter 中,这样的 Widget 被称为 StatelessWidget(无状态组件)

以 Text 的部分源码为例,说明 StatelessWidget 的构建过程。
class Text extends StatelessWidget {
  
  // 构造方法及属性声明部分
  const Text(this.data, {
    key key,
    this.textAlign,
    this.textDirection,
    // 其他参数
    ...
  }) : assert(data != null),
    textSpan = null,
    super(key: key);
  
  final string data;
  final TextAlign textAlign;
  final TextDirection textDirection;
  // 其他属性
  ...
  
  @override
  Widget build(BuildContext context) {
    ... 
    Widget result = RichText(
      // 初始化配置
      ... 
    );
    ...
    return result;
  }
}

代码中可以看到,在构造方法将其属性列表赋值后,build 方法随即将子组件 RichText 通过其属性列表(如文本 data、对齐方式 textAlign、文本展示方向 textDirection 等)初始化后返回,之后 Text 内部不再响应外部数据的变化。

那么,什么应用场景下应该使用 StatelessWidget 呢?

简单的判断规则:父 Widget 是否能够通过初始化参数完全控制其 UI 展示效果? 如果能,那么就可以使用 StatelessWidget 来设计构造函数接口了。

比如错误信息提示的弹框控件,可以采用继承 StatelessWidget 的方式,来进行组件自定义。计数器按钮需要通过继承 StatefulWidget 的方式,来进行组件自定义。

StatefulWidget

有一些 Widget(比如 Image、Checkbox)的展示,除了父 Widget 初始化时传入的静态配置之外,还需要处理用户的交互(比如,用户点击按钮)或其内部数据的变化(比如,网络数据回包),并体现在 UI 上。换句话说,这些 Widget 创建完成后,还需要关心和响应数据变化来进行重绘。在 Flutter 中,这一类 Widget 被称为 StatefulWidget(有状态组件)

StatefulWidget 是以 State 类代理 Widget 构建的设计方式实现的。接下来,以 Image 的部分源码为例,说明 StatefulWidget 的构造过程。

和上面的 Text 一样,Image 类的构造函数会接收要被这个类使用的属性参数。然而,不同的是,Image 类并没有 build 方法来创建视图,而是通过 createState 方法创建来一个类型为 _ImageState 的 state 对象,然后由这个对象负责视图的构建。

这个 state 对象持有并处理来 Image 类中的状态变化,所以就以 _imageInfo 属性为例来展开说明。

_imageInfo 属性用来给 Widget 加载真实的图片,一旦 State 对象通过 _handleImageChanged 方法监听到 _imageInfo 属性发生来变化,就会立即调用 _ImageStage 类的 setState 方法通知 Flutter 框架:“数据变了,请使用更新后的 _imageInfo 数据重新加载图片!”。Flutter 框架则会标记视图状态,更新 UI。

StatefulWidget 不是万金油,要慎用

事实上,StatefulWidget 的滥用会直接影响 Flutter 应用的渲染性能。

Widget 是不可变的,更新则意味着销毁 + 重建(build)。StatelessWidget 是静态的,一旦创建则无需更新;而对于 StateFulWidget 来说,在 State 类中调用 setState 方法更新数据,会触发视图的销毁和重建,也将间接地触发其每个子 Widget 的销毁和重建。

如果根布局是一个 StatefulWidget,在其 State 中每调用一次更新 UI,都将是一整个页面所有 Widget 的销毁和重建。

虽然 Flutter 内部通过 Element 层可以最大程度地降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个 RenderObject 树重建。但,大量 Widget 对象的销毁重建是无法避免的。如果某个子 Widget 的重建涉及到一些耗时操作,那页面的渲染性能将会急剧下降。

因此,正确评估视图展示需求,避免无谓的 StatefulWidget 使用,是提高 Flutter 应用渲染性能最简单也是最直接的手段