Flutter 中 StatelessWidget 与 StatefulWidget 的抉择

前言

在学习 Flutter 中,我们了解到 Widget 有 StatelessWidget 和 StatefulWidget 两种类型。其中 StatefulWidget 对应有交互、需要动态变化视图效果的场景,而 StatelessWidget 则用于处理静态的、无状态的视图展示。下面就对 StatelessWidget 和 StatefulWidget 进行学习和比较,从而更好地理解 Widget,掌握不同类型 Widget 的正确使用时机。


(一)UI 编程范式

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

在 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(无状态组件)。

这里有一张 StatelessWidget 的示意图,如下所示:


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 来设计构造函数接口了。


(三)StatefulWidget

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


StatefulWidget 示意图

接下来,我以 Image 的部分源码为例,和你说明 StatefulWidget 的构建过程。

class Image extends StatefulWidget {
  // 构造方法及属性声明部分
  const Image({
    Key key,
    @required this.image,
    // 其他参数
  }) : assert(image != null),
       super(key: key);

  final ImageProvider image;
  // 其他属性
  ...
  
  @override
  _ImageState createState() => _ImageState();
  ...
}

class _ImageState extends State<Image> {
  ImageInfo _imageInfo;
  // 其他属性
  ...

  void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
      _imageInfo = imageInfo;
    });
  }
  ...
  @override
  Widget build(BuildContext context) {
    final RawImage image = RawImage(
      image: _imageInfo?.image,
      // 其他初始化配置
      ...
    );
    return image;
  }
 ...
}

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

拿 _imageInfo 属性为例,_imageInfo 属性用来给 Widget 加载真实的图片,一旦 State 对象通过 _handleImageChanged 方法监听到 _imageInfo 属性发生变化,就会立即调用 setState 方法通知 Flutter 框架重新构建,更新 UI。

在这个例子中,Image 以一种动态的方式运行:监听变化,更新视图。与 StatelessWidget 通过父 Widget 完全控制 UI 展示不同,StatefulWidget 的父 Widget 仅定义它的初始化状态,而自身视图运行的状态则需要自己处理,并根据处理情况即时更新 UI 展示。


(四)StatefulWidget 不是万金油

这时我们会有疑问,既然 StatefulWidget 不仅可以响应状态变化,又能展示静态 UI,那么 StatelessWidget 这种只能展示静态 UI 的 Widget,还有必要使用吗?

首先,看一下 Widget 的更新机制:

Widget 是不可变的,更新则意味着销毁和重建。StatelessWidget 是静态的,一旦创建则无需更新;而 StatefulWidget 在调用 setState 方法更新数据后,会触发视图的销毁和重建,也将间接地触发其每一个子 Widget 的销毁和重建。

尽管 Flutter 会通过 Element 层最大程度降低对真实渲染视图的修改,但是如果滥用 StatefulWidget,在更新 UI 时,一整个页面所有的 Widget 都会销毁和重建。这样大大降低了渲染性能。

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


总结

由于 Widget 采用由父到子、自顶向下的方式进行构建,因此在自定义组件时,我们可以根据父 Widget 是否能通过初始化参数完全控制其 UI 展示效果的基本原则,来选择继承 StatelessWidget 还是 StatefulWidget。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342