极客时间已完结课程限时免费阅读

10 | Widget中的State到底是什么?

10 | Widget中的State到底是什么?-极客时间

10 | Widget中的State到底是什么?

讲述:陈航

时长12:07大小11.10M

你好,我是陈航。
通过上一篇文章,我们已经深入理解了 Widget 是 Flutter 构建界面的基石,也认识了 Widget、Element、RenderObject 是如何互相配合,实现图形渲染工作的。Flutter 在底层做了大量的渲染优化工作,使得我们只需要通过组合、嵌套不同类型的 Widget,就可以构建出任意功能、任意复杂度的界面。
同时,我们通过前面的学习,也已经了解到 Widget 有 StatelessWidget 和 StatefulWidget 两种类型。StatefulWidget 应对有交互、需要动态变化视觉效果的场景,而 StatelessWidget 则用于处理静态的、无状态的视图展示。StatefulWidget 的场景已经完全覆盖了 StatelessWidget,因此我们在构建界面时,往往会大量使用 StatefulWidget 来处理静态的视图展示需求,看起来似乎也没什么问题。
那么,StatelessWidget 存在的必要性在哪里?StatefulWidget 是否是 Flutter 中的万金油?在今天这篇文章中,我将着重和你介绍这两种类型的区别,从而帮你更好地理解 Widget,掌握不同类型 Widget 的正确使用时机。

UI 编程范式

要想理解 StatelessWidget 与 StatefulWidget 的使用场景,我们首先需要了解,在 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 的设计思路完全一致
对我们来说,如果要实现同样的需求,则要稍微麻烦点:除了设计好 Widget 布局方案之外,还需要提前维护一套文案数据集,并为需要变化的 Widget 绑定数据集中的数据,使 Widget 根据这个数据集完成渲染。
但是,当需要变更界面的文案时,我们只要改变数据集中的文案数据,并通知 Flutter 框架触发 Widget 的重新渲染即可。这样一来,开发者将无需再精确关注 UI 编程中的各个过程细节,只要维护好数据集即可。比起命令式的视图开发方式需要挨个设置不同组件(Widget)的视觉属性,这种方式要便捷得多。
总结来说,命令式编程强调精确控制过程细节;而声明式编程强调通过意图输出结果整体。对应到 Flutter 中,意图是绑定了组件状态的 State,结果则是重新渲染后的组件。在 Widget 的生命周期内,应用到 State 中的任何更改都将强制 Widget 重新构建。
其中,对于组件完成创建后就无需变更的场景,状态的绑定是可选项。这里“可选”就区分出了 Widget 的两种类型,即:StatelessWidget 不带绑定状态,而 StatefulWidget 带绑定状态。当你所要构建的用户界面不随任何状态信息的变化而变化时,需要选择使用 StatelessWidget,反之则选用 StatefulWidget。前者一般用于静态内容的展示,而后者则用于存在交互反馈的内容呈现中。
接下来,我分别和你介绍 StatelessWidget 和 StatefulWidget,从源码分析它们的区别,并总结一些关于 Widget 选型的基本原则。

StatelessWidget

在 Flutter 中,Widget 采用由父到子、自顶向下的方式进行构建,父 Widget 控制着子 Widget 的显示样式,其样式配置由父 Widget 在构建时提供。
用这种方式构建出的 Widget,有些(比如 Text、Container、Row、Column 等)在创建时,除了这些配置参数之外不依赖于任何其他信息,换句话说,它们一旦创建成功就不再关心、也不响应任何数据变化进行重绘。在 Flutter 中,这样的 Widget 被称为 StatelessWidget(无状态组件)
这里有一张 StatelessWidget 的示意图,如下所示:
图 1 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 来设计构造函数接口了。
我准备了两个简单的小例子,来帮助你理解这个判断规则。
第一个小例子是,我需要创建一个自定义的弹窗控件,把使用 App 过程中出现的一些错误信息提示给用户。这个组件的父 Widget,能够完全在子 Widget 初始化时将组件所需要的样式信息和错误提示信息传递给它,也就意味着父 Widget 通过初始化参数就能完全控制其展示效果。所以,我可以采用继承 StatelessWidget 的方式,来进行组件自定义。
第二个小例子是,我需要定义一个计数器按钮,用户每次点击按钮后,按钮颜色都会随之加深。可以看到,这个组件的父 Widget 只能控制子 Widget 初始的样式展示效果,而无法控制在交互过程中发生的颜色变化。所以,我无法通过继承 StatelessWidget 的方式来自定义组件。那么,这个时候就轮到 StatefulWidget 出场了。

StatefulWidget

与 StatelessWidget 相对应的,有一些 Widget(比如 Image、Checkbox)的展示,除了父 Widget 初始化时传入的静态配置之外,还需要处理用户的交互(比如,用户点击按钮)或其内部数据的变化(比如,网络数据回包),并体现在 UI 上。
换句话说,这些 Widget 创建完成后,还需要关心和响应数据变化来进行重绘。在 Flutter 中,这一类 Widget 被称为 StatefulWidget(有状态组件)。这里有一张 StatefulWidget 的示意图,如下所示:
图 2 StatefulWidget 示意图
看到这里,你可能有点困惑了。因为,我在上一篇文章“Widget,构建 Flutter 界面的基石”中和你分享到,Widget 是不可变的,发生变化时需要销毁重建,所以谈不上状态。那么,这到底是怎么回事呢?
其实,StatefulWidget 是以 State 类代理 Widget 构建的设计方式实现的。接下来,我就以 Image 的部分源码为例,和你说明 StatefulWidget 的构建过程,来帮助你理解这个知识点。
和上面提到的 Text 一样,Image 类的构造函数会接收要被这个类使用的属性参数。然而,不同的是,Image 类并没有 build 方法来创建视图,而是通过 createState 方法创建了一个类型为 _ImageState 的 state 对象,然后由这个对象负责视图的构建。
这个 state 对象持有并处理了 Image 类中的状态变化,所以我就以 _imageInfo 属性为例来和你展开说明。
_imageInfo 属性用来给 Widget 加载真实的图片,一旦 State 对象通过 _handleImageChanged 方法监听到 _imageInfo 属性发生了变化,就会立即调用 _ImageState 类的 setState 方法通知 Flutter 框架:“我这儿的数据变啦,请使用更新后的 _imageInfo 数据重新加载图片!”。而,Flutter 框架则会标记视图状态,更新 UI。
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 以一种动态的方式运行:监听变化,更新视图。与 StatelessWidget 通过父 Widget 完全控制 UI 展示不同,StatefulWidget 的父 Widget 仅定义了它的初始化状态,而其自身视图运行的状态则需要自己处理,并根据处理情况即时更新 UI 展示。
好了,至此我们已经通过 StatelessWidget 与 StatefulWidget 的源码,理解了这两种类型的 Widget。这时,你可能会问,既然 StatefulWidget 不仅可以响应状态变化,又能展示静态 UI,那么 StatelessWidget 这种只能展示静态 UI 的 Widget,还有存在的必要吗?

StatefulWidget 不是万金油,要慎用

对于 UI 框架而言,同样的展示效果一般可以通过多种控件实现。从定义来看,StatefulWidget 仿佛是万能的,替代 StatelessWidget 看起来合情合理。于是 StatefulWidget 的滥用,也容易因此变得顺理成章,难以避免。
但事实是,StatefulWidget 的滥用会直接影响 Flutter 应用的渲染性能。
接下来,在今天这篇文章的最后,我就再带你回顾一下 Widget 的更新机制,来帮你意识到完全使用 StatefulWidget 的代价:
Widget 是不可变的,更新则意味着销毁 + 重建(build)。StatelessWidget 是静态的,一旦创建则无需更新;而对于 StatefulWidget 来说,在 State 类中调用 setState 方法更新数据,会触发视图的销毁和重建,也将间接地触发其每个子 Widget 的销毁和重建。
那么,这意味着什么呢?
如果我们的根布局是一个 StatefulWidget,在其 State 中每调用一次更新 UI,都将是一整个页面所有 Widget 的销毁和重建。
在上一篇文章中,我们了解到,虽然 Flutter 内部通过 Element 层可以最大程度地降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个 RenderObject 树重建。但,大量 Widget 对象的销毁重建是无法避免的。如果某个子 Widget 的重建涉及到一些耗时操作,那页面的渲染性能将会急剧下降。
因此,正确评估你的视图展示需求,避免无谓的 StatefulWidget 使用,是提高 Flutter 应用渲染性能最简单也是最直接的手段
在接下来的第 29 篇文章“为什么需要做状态管理,怎么做?”中,我会继续带你学习 StatefulWidget 常见的几种状态管理方法,与你更为具体地介绍在不同场景中,该选用何种 Widget 的基本原则。这些原则,你都可以根据实际需要应用到后续工作中。

总结

好了,今天关于 StatelessWidget 与 StatefulWidget 的介绍,我们就到这里了。我们一起来回顾下今天的主要知识点。
首先,我带你了解了 Flutter 基于声明式的 UI 编程范式,并通过阅读两个典型 Widget(Text 与 Image)源码的方式,与你一起学习了 StatelessWidget 与 StatefulWidget 的基本设计思路。
由于 Widget 采用由父到子、自顶向下的方式进行构建,因此在自定义组件时,我们可以根据父 Widget 是否能通过初始化参数完全控制其 UI 展示效果的基本原则,来判断究竟是继承 StatelessWidget 还是 StatefulWidget。
然后,针对 StatefulWidget 的“万金油”误区,我带你重新回顾了 Widget 的 UI 更新机制。尽管 Flutter 会通过 Element 层去最大程度降低对真实渲染视图的修改,但大量的 Widget 销毁重建无法避免,因此避免 StatefulWidget 的滥用,是最简单、直接地提升应用渲染性能的手段。
需要注意的是,除了我们主动地通过 State 刷新 UI 之外,在一些特殊场景下,Widget 的 build 方法有可能会执行多次。因此,我们不应该在这个方法内部,放置太多有耗时的操作。而关于这个 build 方法在哪些场景下会执行,以及为什么会执行多次,我会在下一篇文章“提到生命周期,我们是在说什么?”中,与你一起详细分析。

思考题

Flutter 工程应用模板是计数器示例应用 Demo,这个 Demo 的根节点是一个 StatelessWidget。请在保持原有功能的情况下,将这个 Demo 改造为根节点为 StatefulWidget 的 App。你能通过数据打点,得出这两种方式的性能差异吗?
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 7

提建议

上一篇
09 | Widget,构建Flutter界面的基石
下一篇
11 | 提到生命周期,我们是在说什么?
 写留言

精选留言(43)

  • 加温后的啤酒
    2019-07-22
    老师,有一个疑问没有理解。你文中提到如果根布局是StatefulWidget,如果调用setState,就会触发子widet的销毁和重建,影响性能。但是在真实业务场景中,我把跟控制器写成StatefulWidget,但我默认他是不可变的,所以我肯定不会主动去调用setState方法啊,那如果我不主动调用setState的话,那不就不会有应能影响了吗。这没法说明StatelessWidget的存在是必要的的??老师能解释一下吗?

    作者回复: 实际上你即使不去主动setState,对于Stateful在特定的时机也会rebuild的。具体可以参看下一篇文章

    共 2 条评论
    36
  • JakePrim
    2019-07-20
    就喜欢这种讲解加举例的方式,非常清晰
    19
  • Mr.J
    2019-07-21
    构建界面时,抛开业务,光看界面,把界面按层次拆分,需要动态更新的地方,用Stateful,然后将其统一放在Stateless中,做到例如在一个小区域中,根布局也是Stateless,其中有一个控件为stateful,刷新时只刷新这个小部分,这样吗老师

    作者回复: 是的

    10
  • JW
    2019-09-27
    Element是Widget层的一个抽象用来处理真正需要重建的的Widget,它是如何来决定谁要重建谁不要重建的逻辑的?

    作者回复: 1.Widget通知Element重建的触发时机,可以参考第11篇生命周期的分享。 2.一旦Widget触发重建,Element会根据重建前后Widget树的渲染类型及属性变化情况,决定后续的复用、新建过程:比如Widget树中仅仅是调整了一个渲染样式,Flutter会通知Element直接复用现有节点,同步属性至RenderObject,触发绘制即可;如果Widget树中涉及到Widget类型的替换或变更,Flutter则会将老的Element及RenderObject摘除,让新的Widget重新走一遍创建Element和RenderObject的流程,挂载到Element树和RenderObject树上。

    8
  • (☆_☆)
    2019-07-20
    简单来说StatelessWidget就是为了提升性能而被设计出来,而完全使用StatefulWidget可能对性能有影响,所以在使用前一定要评估一下用哪个比较合适,这样理解对吗?

    作者回复: 是的

    7
  • Lgh
    2021-09-27
    有个比较尴尬的问题,我想问问老师,为什么text是statelesswidget?Text的文案变化不应该属于可变的吗?
    共 2 条评论
    3
  • 、轻
    2019-08-14
    这两个widget与react中的容器和组件很类似

    作者回复: 是类似的概念

    3
  • 格格
    2019-08-05
    现在Image里已经找不到_handleImageChanged方法了,好像被_handleImageFrame取代了

    作者回复: 确实,1.5还有,1.7已经把这个方法替换掉了。

    3
  • 承香墨影
    2019-07-30
    老师,你好,有个疑问希望解答。 既然 StatefulWidget 需要区分场景来使用,并且 Widget 的销毁和重建应该是 Flutter 的常态。那么在使用中,应该将 StatefulWidget 尽量的拆小,让其影响范围,尽可能的小。 这是不是就对应到 “04 | Flutter 区别于其他方案的关键技术是什么” 中讲到的 布局边界 和 重绘边界 概念。其实在实际代码中,是依赖 StatelessWidget 进行设定边界,从而隔离布局和重绘的?
    展开

    作者回复: 不是的哈。 1.重绘边界是解决同层Widget(有兄弟、有父子)之间渲染依赖出现的概念:即只要他们享用了同一个layer,则无论哪一个需要重绘,整个layer都会受到影响。 2.StatefulWidget则影响的是其子节点,一般情况下只影响重建,Element会在底层做diff,确保没有修改的不会重绘

    3
  • 矮个子先生😝
    2019-08-13
    ``` const Image({ Key key, @required this.image, // 其他参数 }) : assert(image != null), super(key: key); ``` 老师可以介绍下这个构造方法吗, 第一个Key key, : assert(image != null),super(key: key); 这三部分
    展开

    作者回复: 1.key用在Element复用过程中,控制控件如何取代树中的另一个控件。比如你在父Widget用新的image重建了Image,底层Element还是能复用的。 2.assert是断言,只在debug中生效。 3.super我们在Dart里面讲过,是调用父类的构造方法

    2
  • G
    2019-07-22
    我查了下资料,好像是说虽然widget是不可变的,但是state是可变的,也就是说state实例会被复用,并且在setstate重新生成widget树时会检查节点是否有变化,没有变化就停止遍历。另外我认为stateful的实例相比stateless更轻,毕竟没有build方法。 Ps: 在递归下降生成子树的时候,我有个疑问,flutter如何判断子树一样呢?算法是如何的?

    作者回复: 靠类型和key

    共 2 条评论
    2
  • 曾家二女婿
    2020-01-10
    【思考题】 void main() => runApp(MyApp()); class MyApp extends StatefulWidget { MyApp({Key key}) : super(key: key); @override _MyApp createState() => _MyApp(); } class _MyApp extends State<MyApp> { bool check = true; void _updateIndeView() { setState(() { check = !check; }); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', // 应用的名字 theme: ThemeData( // 蓝色主题 primarySwatch: Colors.red, ), // 首页路由 home: Scaffold( // 点击 切换 主页 body: Container( child: InkWell( onTap: _updateIndeView, child: check ? MyHomePage(title: 'Flutter Demo Home Page') : _MyHomePageDemo(), ) // check ? MyHomePage(title: 'Flutter Demo Home Page') : _MyHomePageDemo(), ), ), ); } }
    展开
    1
  • Captain
    2019-11-14
    有个问题请教“虽然 Flutter 内部通过 Element 层可以最大程度地降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个 RenderObject 树重建。但,大量 Widget 对象的销毁重建是无法避免的”这里 如果根节点用了Stateful,根节点setState,来改变其中变化的子节点状态(子节点状态中没有耗时操作),那Element会帮助diff出变化的子节点,避免重新构建,这样也不影响性能呀?

    作者回复: “但,大量的Widget对象的销毁重建是无法避免的”

    1
  • 严旭珺
    2019-08-08
    感觉项目的一个优化方向就是尽量用statelesswidget

    作者回复: 是的

    1
  • 吴小安
    2019-08-07
    请问这种声明式编程在ios和安卓业界有没有简单的框架能用的? 感觉现在ios使用的面向数据开发也都是命令式编程,界面绑定某个值,kvo变化了在回调里做刷新ui的操作,怎样能向这种声明式转移

    作者回复: Litho和Texture(AsyncDisplayKit)算一个,不过框架整体比较重,有一定学习成本

    共 2 条评论
    1
  • 徐西文
    2019-07-23
    Dart为什么不设计一种可以自动选择状态的widget

    作者回复: 因为simple is best,另外Element已经拦截不必要的绘制了

    1
  • Bula
    2019-07-22
    StatefulWidget感觉很难减少使用频率啊 现在的设计标题栏的标题都是要跟着状态改变动态更改 😓

    作者回复: 拆小就行了

    1
  • Eren
    2019-07-20
    学到现在,真的是受益匪浅,之前的一些疑惑都从中得到了答案,有种恍然大悟的感觉,希望老师能在未来的学习中,分享一下 Flutter 的从业情况和面试题,感谢之至。

    作者回复: 会讲一部分

    1
  • Egos
    2019-07-20
    思考题里面是将MyApp 替换成StatefulWidget,然后需要在点击FloatingActionButton 的时候更新MyApp 的State 么?这样需要将MyApp 的State 一直传递到_MyHomePageState?

    作者回复: 是的

    1
  • 张简
    2019-07-20
    清楚明白
    1