【Flutter脱发实录】盘一盘Widget

499 阅读7分钟

在Flutter的学习过程中,听到看到最多的一个概念就是Widget。可以说,在Flutter宇宙中:Everything is widget. 机器翻译一下就是:一切都是小玩意儿。这个小玩意儿到底是个啥?咱们来盘一盘!

官方解释

Describes the configuration for an [Element].

Widget 描述了Element的配置。

并且!源码中有这么一句注释:Widgets are the central class hierarchy in the Flutter framework. Widget是个啥???Flutter框架中的C位担当!!!

Element同样是一位咖位很高的角色,但是我们暂时先放一放,先来看看这个C位出道的Widget

凭什么?

刚开始学习Flutter开发,最先接触到的可能就是TextContainerRowScaffoldGestureDetector等。其实这些都是Widget,只是有各自不同的分工而已。

开发一个App,就是使用一个个相同的、不同的Widget堆叠起来的一个过程。这也体现了Flutter的一个非常重要的理念 组合 。这些Widget中,有些是最小的小玩意儿,有些是由几个小玩意儿拼装在一起的大玩意儿。

把开发一款App比作造一辆车。我们只需要使用如下几个现成零部件:轮子,座椅,把手,发动机,连接杆,拼接好就可以,而不用关心发动机是如何工作的,轮胎是怎么能滚动的。

用两个轮子+人力发动机+一根连接杆就能做成一辆自行车。

用四个轮子+汽油发动机+三根连接杆就能做成一辆汽车。

找到老父亲的皮夹克,裁一裁套在坐凳上,坐凳变成了一个新的部件-皮坐凳,那么最后出厂的就是辆豪华真皮自行车了。

再买来一桶红油漆,一通乱刷,法拉利来了。

再装个竹蜻蜓,就是想上天了。。。

是的,嘴炮造车就是这么简单,同样开发Flutter App也是这么简单。Flutter为你提供了一所超级工厂,Widget就是你车间展柜上的半成品,你不用理解实现细节,就可以收获马斯克式快感。你说这个C位坐的实不实?

阅读源码

Widget的源码非常简单,把几个关键的摘了出来:

@immutable
abstract class Widget extends DiagnosticableTree {
  
  const Widget({ this.key });
  
  final Key key;
  
  @protected
  Element createElement();
  
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}
  • @immutable注解表明Widget是不可变的,所有的属性必须为final修饰
  • 继承自DiagnosticableTree表明Flutter中存在一棵Widget树,而Widget就是这棵树上的对象。
  • Key是区分一棵树上不同Widget的标志
  • createElement()生成一个Element对象
  • canUpdate这个方法决定该widget的重建方式,具体区别会在Element篇中提到。

四大弟子

Widget主要有四个子类,特点鲜明。Flutter中大部分的Widget都是继承自它们并且用它们加以组合。

RenderObjectWidget

顾名思义,Widget是一个抽象的配置文件,RenderObjectWidget就是配置中带一个RenderObject的文件。

RenderObject是什么?后续的文章中会详细说明。暂时可以把它当成一个干实事的,细粒度的东西。比如弹簧本簧,螺丝钉本钉,内胎本胎,油漆本漆等等。当一个Widget需要去做layoutpaint发挥实际功效时,就需要拥有它。

RenderObjectWidget就是对其的一个抽象,可以理解为泛指。通过方法RenderObject createRenderObject(BuildContext context)规定了其指定的一个RenderObject注意:这里只是规定,并没有创建RenderObject,别被方法名误导了。

RenderObjectWidget下根据其允许接入的child的个数也产生了三个分支:

  • LeafRenderObjectWidget

不能有child即表示此Widget只能是Widget Tree上的叶节点。例如图片控件:

Image(StatefulWidget) 
-> RawImage(LeafRenderObjectWidget) 
-> 指定RenderImage作为其RenderObject

所以Image常常存在于dart回调地狱的最底层。

  • SingleChildRenderObjectWidget

只规定有一个childRenderObjectWidget,拥有属性final Widget child,可以从构造器中传入一个Widget

大部分RenderObjectWidget都继承自这个类,例如透明度控件:

Opacity(SingleChildRenderObjectWidget) 
-> 指定RenderOpacity作为其RenderObject
  • MultiChildRenderObjectWidget

可以有多个child,拥有属性final List<Widget> children,可以从构造器中传入多个Widget。例如:

Row/Column
-> Flex(MultiChildRenderObjectWidget)
-> 指定RenderFlex作为其RenderObject
Stack(MultiChildRenderObjectWidget)
-> 指定RenderStack作为其RenderObject

StatelessWidget

一种状态不可变的Widget

是最简单的Widget,提供build方法,整合一个或者多个Widget并返回一个新的Widget

这就是一个组合的过程,使用现有的零件,封装成一个新的零件。

最标志性的控件就是Container了。不知道你们有没有这种感觉,在Flutter开发过程中,总感觉其无所不能,可以控制各种各样的属性,实现各种各样的效果。Ctrl+Click一下一目了然。

class Container extends StatelessWidget {
  Container({
    Key key,
    ... //很多很多参数
  }) : super(key: key);
  
  final ... // 很多很多属性

  @override
  Widget build(BuildContext context) {
    Widget current = child;

    if (child == null && (constraints == null || !constraints.isTight)) {
      current = LimitedBox(
        maxWidth: 0.0,
        maxHeight: 0.0,
        child: ConstrainedBox(constraints: const BoxConstraints.expand()),
      );
    }

    if (alignment != null)
      current = Align(alignment: alignment, child: current);

    final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
    if (effectivePadding != null)
      current = Padding(padding: effectivePadding, child: current);

    if (color != null)
      current = ColoredBox(color: color, child: current);

    if (decoration != null)
      current = DecoratedBox(decoration: decoration, child: current);

    if (foregroundDecoration != null) {
      current = DecoratedBox(
        decoration: foregroundDecoration,
        position: DecorationPosition.foreground,
        child: current,
      );
    }

    if (constraints != null)
      current = ConstrainedBox(constraints: constraints, child: current);

    if (margin != null)
      current = Padding(padding: margin, child: current);

    if (transform != null)
      current = Transform(transform: transform, child: current);

    if (clipBehavior != Clip.none) {
      current = ClipPath(
        clipper: _DecorationClipper(
            textDirection: Directionality.of(context),
            decoration: decoration
        ),
        clipBehavior: clipBehavior,
        child: current,
      );
    }

    return current;
  }
}

可以看到build方法中的逻辑,非常简单,根据属性值,将child用各种各样的Widget一层一层的包裹起来,最后返回了一个新的Widget

就像一块面包,根据配料,加一层肉,就变成了汉堡,加一层西红柿,就变成了营养汉堡,再加两层肉,就变成了三倍快乐堡。

Container就是这样一个DIY菜单,给你提供在给定范围内自由选择的空间。

利用StatelessWidget就可以利用一些基础的食材,创造麻辣烫菜单,奶茶菜单等等。(指配料自选,芝芝莓莓,三兄弟,什么都有之类的茶品其实已经是一个比较完善的StatelessWidget了)

StatefulWidget

StatefulWidget的区别就在于,他拥有属于自己的局部状态管理机制State

State的生命周期_StateLifecycle有四种状态:

  • created State被创建,State.initState初始化方法在此时被调用
  • initialized State.initState初始化方法执行完毕,但还没有准备好build。此时State.didChangeDependencies方法被调用
  • ready 已经准备好buildState.dispose方法未被调用
  • defunct State.dispose方法被调用,失去build能力

State最重要的一个方法就是setState(){},用来重构Widget,如何重构暂且不提。

看到上述一大堆东西,可能你乱了。但是举个例子你就明白了。

StatelessWidget就像是一张纸质的菜单,预置了几种可变项之后就不能改变了。

StatefulWidget就是一块电子屏菜单,可以根据不同的状态,刷新一下变成早餐菜单,午餐菜单,夜宵菜单,或者是夏日菜单和冬季菜单。

State生命周期就相当于电子屏的网络状态,开机,连接网络,匹配菜单状态更新菜单,断网失去更新能力。

ProxyWidget

当父子控件,需要传递信息时,就需要使用ProxyWidget了。 主要有两种情况:

  • InheritedWidget

官方提供的允许子类访问和修改父类属性的基础控件。但是只允许向下传递,子类访问父类。看个官方栗子:

class FrogColor extends InheritedWidget {
   const FrogColor({
     Key key,
     @required this.color,
     @required Widget child,
   }) : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

   final Color color;

   static FrogColor of(BuildContext context) {
     return context.dependOnInheritedWidgetOfExactType<FrogColor>();
   }
}

通过FrogColor.of(BuildContext context)方法获取到父类对象和属性。

是不是想起了MediaQuery

使用这个思想可以实现全局状态的效果,但是实现起来有点复杂,效果也不好,所以诞生很多的替代方案比如Provider,FishRedux等等。

  • ParentDataWidget

当父控件需要给子类传递信息时,会将传递的信息装在ParentData中,传给childParentDataWidget由此得名。

RenderObject有一个存储信息的属性parentData,当Widget需要申请访问这个数据时,就需要继承ParentDataWidget通过void applyParentData(RenderObject renderObject) {}方法去访问这个数据。而ParentData中到底会存储哪些信息,在RenderObject篇中会有介绍。

最常见的两个案例就是:

Expanded -> Flexible -> ParentDataWidget<FlexParentData>
Positioned -> ParentDataWidget<StackParentData>

总结

Widget本身的内容并不多,也比较好理解。我们所接触到的Widget都是Flutter已经封装好功能的一个个配件,我们只要按照其规则进行组装,搭配,就能得到自己想要效果。

以上均为个人愚见,若有错误和不足之处,欢迎指正!

最后祈祷我身上流淌的贵族血脉不易脱发!