前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Widget中的state到底是什么

Widget中的state到底是什么

作者头像
拉维
发布2019-08-12 16:00:17
2.9K0
发布2019-08-12 16:00:17
举报
文章被收录于专栏:iOS小生活

在上一篇文章Widget,构建Flutter界面的基石中,我们深入理解了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编程范式

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

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

代码语言:javascript
复制
// 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的视图开发是声明式的,其核心设计思想就是将视图和数据分离。在Flutter中,如果要实现上述同样的需求,则要稍微麻烦点:除了设计好Widget布局方案之外,还需要提前维护一套文案数据集,并为需要变化的widget绑定数据集中的数据,使Widget根据这个数据集完成渲染。

但是,当需要变更界面的文案时,我们只要改变数据集中的文案数据,并通知Flutter框架触发Widget的重新渲染即可。这样一来,开发者将无需精确关注UI编程中的各个过程细节,只要维护好数据集即可。比起命令式的视图开发方式需要挨个设置不同组件(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的示意图,如下所示:

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

代码语言:javascript
复制
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的示意图,如下所示:

看到这里你可能有点困惑了。因为,之前我们提到,Widget是不可变的,发生变化时需要销毁重建,所以谈不上状态。那么,这到底是怎么回事呢?

其实,StatefulWidget是以State类代理Widget构建的设计方式实现的。接下来,我就以Image的部分源码为例,和你说明StatefulWidget的构建过程,来帮助你理解这个知识点。

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

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

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

代码语言:javascript
复制
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的更新机制:

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

那么,这意味着什么呢?

如果我们的根布局是一个StatefulWidget,在其State中每调用一次更新UI,都将是一整个页面所有Widget的销毁和重建。这里你可能会有疑问,如果我在一个默认不可变的场景下使用StatefulWidget,那么我肯定不会主动调用其setState方法啊,如果我不主动调用setState,那么不就不会影响StatefulWidget的更新了吗?但是实际上,即使你不去主动setState,StatefulWidget在特定的时机也会rebuild的,这一点我在下一篇文章中会做详细介绍。

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

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

总结

在iOS、Android以及JavaScript中,视图开发都是命令式的;而在Flutter中,视图开发则是声明式的,我们只需要改变数据,然后通过Flutter框架触发Widget的重新渲染即可。

Flutter中,Widget分为StatelessWidget和StatefulWidget。

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

如果我们的根布局是一个StatefulWidget,在其State中每调用一次更新UI,都将是一整个页面所有Widget的销毁和重建。虽然Flutter内部可以通过Element层最大程度地降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个RenderObject树重建。但是大量Widget对象的销毁重建却是不可避免的。如果某个子Widget的重建涉及到一些耗时操作,那页面的渲染性能将会急剧下降。所以,一定要避免StatefulWidget的滥用。

以上。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档