专栏名称: 阿辉_
目录
相关文章推荐
太格有物  ·  新品快讯|UNITED ... ·  2 天前  
太格有物  ·  太格玩家|JOSEPHINE ... ·  3 天前  
51好读  ›  专栏  ›  阿辉_

Flutter中的Widget

阿辉_  · 掘金  ·  · 2019-08-13 11:32

正文

阅读 50

Flutter中的Widget

Flutter中的Widget

Widget是Flutter框架里核心类。在Flutter里最核心的是用widgets构建UI界面。widgets描述的UI界面的长相及当前的配置与状态。当widgets的状态改变后,widgets将重构它的描述,框架会与前一个描述做比对,对渲染树从前一个状态到当前的状态做出最小的改变。

在Native开发中,View就代表一个渲染类,是要最终由渲染管线渲染到屏幕上去的,所以比较重。而在Flutter当中Widget是用来描述UI的不可变数据结构。初学者很容易会把它当作一个View来使用,会不自觉得持有并复用它(主要还是Native的思维造成,以为Widget和View一样比较重)。事实上Widget就一数据结构,创建与销毁都比较轻量(尤其Dart语言专门为它优化过),所以尽量根据数据状态生成Widget即可(当然在需要考虑性能的地方,可以缓存或者使用const Widget)。

用Flutter开发界面,要理解Flutter的响应式开发。概括来说就是当UI改变时,我们给出一个此刻UI的快照(即Widget Tree),Flutter引擎拿到该快照会自动与前一刻的快照作比对,需要创建的就创建,可以复用的复用,能删除的就删除,最终自动渲染出UI。

所以在Flutter开发中,我们不太关心UI当中的局部刷新调整的细节(比如add, remove update等),关心的是任一时刻与数据状态对应的整体UI快照。

这种整体的思维更符合人的思维,只是因为之前的UI开发框架不够聪明,导致我们一直采用命令式编程,一时不习惯而已,适应了Flutter的思考模式,开发效率一定会有很大的提升。

这篇教程只涉及Flutter中的Widget,对Widget作详细的解释。

源码基于Flutter1.7.8

Widget

是其它Widget的基类,先看源码

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}
复制代码

Widget继承关系:

Widget : DiagnosticableTree : Diagnosticable

Diagnosticable及DiagnosticableTree都是很轻量的,提供了一些诊断及调试方法,所以从源码可以看出Widget是很轻量的,它仅仅是对UI中的一小部分的描述或者配置。所以Widget是轻量的,短暂的。框架会调用createElement()生成Element,Element则是比较持久的,可变的。Element管理渲染树。

Widget被注释为immutable即不可变的,所以Widget没有任何可变的状态,所有的字段都应该是final

传统的View在View Tree中只能出现一次,而Widget可以在Widget Tree出现多次,0到多次。同一个Widget可以在Widget Tree被复用多次,但每次都会生成与Widget相对应的不同的Element。

源码中只提供了key这个字段,Flutter当中key用来控制Element树当中新旧Widget的替换。Flutter通过canUpdate判断,如果两个Widget的runtimeTypekey都是相等的,其所对应的Element就会用新的Widget替换旧的Widget(通过调用Element.update,传递新的Widget),否则,旧的Element要从树中移除,新的Element要添加到树当中。

普通的key只能使同一个位置的Element达到复用效果,开发当中肯定有希望跨不同位置Element复用的情形(比如该Element重建很重,或者需要该位置Element能保持状态)。GlobalKey则可以允许element在树当中移动时(改变父element)不丢失状态。当一个新的WidgetB,所对应位置的element的旧的widgetA与它不匹配(key及type都不符合),但是在前一帧有一个WidgetC的global key与它相同,则WidgetC对应的element会移到widgetB的位置,从而达到复用。

Widget是基类,一个典型的树型结构当中,需要有叶子结点,需要有容器结点,也许还需要有一些特殊结点(比如用来跨结点传递数据)。Flutter提供的容器类Widget有StatelessWidgetStatefullWidget,这两者可以管理一组Widget,还提供了InheritedWidget用于跨节点数据传递,而像Image等则是叶子结点。

StatelessWidget

StatelessWidget是无状态的Widget,源码如下:

abstract class StatelessWidget extends Widget {
  const StatelessWidget({ Key key }) : super(key: key);

  @override
  StatelessElement createElement() => StatelessElement(this);

  @protected
  Widget build(BuildContext context);

}
复制代码

相比WidgetStatelessWidget提供了build方法,通过build方法构建一组Widget来更具体地描述UI的一部分。整个Widget树的构建过程是递归的,直到生成一棵完整具体的UI描述树。

框架会用build方法生成的一组widgets替换(更新或者删除)该widget自己的子树,

StatelessWidget应该在什么场景下使用呢?该部分UI的描述完全可以由该Widget自己的配置信息及构建上下文(BuildContext,即该Widget自己对应的Element)描述,不依赖其它任何外部信息。这也意味着该Widget是无状态的。而需要动态改变的,比如改变需要内部的一个时钟驱动,又或者改变依赖于系统的状态,这时就需要考虑使用有状态的StatefullWidget

build方法的调用有三种时机

  • StatelessWidget第一次插入到树中时(其所对应的Element首次插入到Element树中时)
  • 父对象改变了StatelessWidget的配置
  • 依赖的InheritedWidget发生改变

出于性能的考虑,需要减少build方法的频繁调用及提高build方法的效率。

如果父对象是有规律地改变StatelessWidget的配置,或者依赖的InheritedWidget频繁地变动,为了能有一个平滑的渲染性能,需要优化build的性能。

为了减少重建无状态Widget对性能的冲击,有以下技术可以采用:

  • 减少层级,不仅要最小化build方案构建的Widget层级,这些被构建的Widget自身的层级也要最小化。比如一个子节点需要特别设定的方式去定位,应该使用Align或者CustomSingleChildLayout,避免使用这些复杂的排列如Row,Column,Padding,SizedBox等。如果绘制图形效果需要多个ContainerDecoration组成复杂的分层效果,那应该考虑使用单个CustomPaintWidget。
  • 尽可能使用const widget,并且提供一个const的widget构造函数。const声明的值在编译时其值是确定的,如果值相同,const会引用相同值,避免重复创建。
  • 考虑使用StatefullWidget重构。这样可以使用StatefullWidget的一些优化技术,比如缓存子树或者使用GlobalKey当树的结构变化时。
  • 分拆成多个Widget。如果频繁的重构是由InheritedWidget导致的,可以考虑拆分成多个Widget,将需要改变的部分放到树的叶子节点中。

StatefulWidget

StatefulWidget是一个有可变状态的Widget

先看源码

abstract class StatefulWidget extends Widget {

  const StatefulWidget({ Key key }) : super(key: key);

  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  State createState();
}
复制代码

State用于StatefulWdiget的内部状态及逻辑。

State是应用程序中一段信息,当widget需要构建时可以同步读取该信息,在widget的生命周期里,State会发生变动。当State变动后,需要确保widget可以马上收到通知,调用State.setState

StatelessWidget相同,StatefulWidget也是通过创建一组widget来描述UI中的一部分,它的构建过程也是递归的,直到UI的具体的描述能完全生成。不同的是StatelessWidget通过build方法生成这组widget,而StatefulWidget则通过State.build来生成。

当UI描述的一部分是动态改变时,就可以使用StatefulWidget。比如一个内部时钟驱动的状态或者依赖于系统的状态。

前面说过widget都是不可变的,所以StatefulWidget本身是不可变的,可变的状态都存储在State对象,系统通过调用createState创建独立的State。可变的状态也可能在State订阅的对象中,比如SatefulWidget本身的一引起字段引用Stream或者ChangeNotifier

当需要创建StatefulWidget对应的Element对象时,框架会调用createState创建State。如果同一个StatefulWidget被多次插入到widget树中,State也会被创建多份。周样的,如果StatefulWidget从树中移除随后又再次插入到树中,框架会重新调用createState生成一份新的State,这样可以简化State的生命周期。

StatefulWidget从一个位置移到另一个位置,显示框架会重新调用createState创建新的State,这样状态会丢失。怎么样可以做到复用状态呢?答案就是GlobalKey,使用了GlobalKey,会复用保持该State,从而不丢失状态。具有GlobalKey的widget在树中最多只有一个位置可使用,最多只有一个与之关联的Element。当具有GlobalKey的widget从一个位置移到另一个位置时,框架可以利用该有利条件,将旧位置上widget的子树嫁接到新位置widget的子树上,而不是直接重构新位置上widget的子树,这样,State也跟着从旧位置嫁接复用到新位置上。尽管如此,为了能顺利实现复用,新旧位置的移动必须是在同一动画帧中。

StatefulWidget有两种主要使用类型。

第一种是只在[State.initState]分配资源并且在[State.dispose]中销毁资源,不依赖于InheritedWidget,并且不会调用State.setState。这种类型一般用于程序或者页面的根节点,通过ChangeNotifier或者Stream等与子widgets交互。这种模式代价比较低(在CPU且GPU周期方面),因为它只要构建一次,再也不会刷新并重构。它们一般有一引起比较复杂且深度的build方法。

每二种widget会依赖InheritedWidget或者会调用State.setState(可能同样会合用State.initState或者State.didChangeDependences)。在整个应用生命周期当中它们会被多资重构,所以基于性能的考虑,需要将重构的影响最小化。

为了减少重构的影响,有以下几种技术可以采用:

  • 将State推到叶子结点上。比如,如果一个页面是时钟驱动的,创建一个专用的时钟Widget只更新它自己,而不是将该State放到页面的顶部,当收到时钟信息号时整个页面将要重构。
  • 减少build方法产生的widgets层级
  • 如果子树没有改变,缓存代表该子树的widget并每次复用
  • 尽可能使用const widgets。(如前面解释过,这样会缓存widgets并复用它)
  • 避免改变子树的深度或者改变子树中widget的类型。比如,相比返回子结点或者返回用IgnorePointer包装的子结点,最好一直返回用IgnorePointer包装的子结点,并控制IgnorePointer.ignoring的属性。这是因为改变子树的深度将导致整棵子树的重建,重新布局及重新绘制。而改变属性仅仅要求渲染树很小的改动(这个例子中,不会有重新布局,不会有重新绘制)
  • 如果深度因为一些原因必须要改变,可考虑将子树中的公共部分用具有GlobalKey的Widget包装起来,该GlobalKeyStatefulWidget生命周期里保持一致。(如果其它widget的key不方便赋值,KeyedSubtree就可以派上用场了)

State

@optionalTypeArgs
abstract class State<T extends StatefulWidget> extends Diagnosticable {
  
  T get widget => _widget;
  T _widget;

  _StateLifecycle _debugLifecycleState = _StateLifecycle.created;

  
  bool _debugTypesAreRight(Widget widget) => widget is T;

  
  BuildContext get context => _element;
  StatefulElement _element;

  bool get mounted => _element != null;

  @protected
  @mustCallSuper
  void initState() {
    assert(_debugLifecycleState == _StateLifecycle.created);
  }

  @mustCallSuper
  @protected
  void didUpdateWidget(covariant T oldWidget) { }

  @protected
  @mustCallSuper
  void reassemble() { }

  @protected
  void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }

  
  @protected
  @mustCallSuper
  void deactivate() { }

  
  @protected
  @mustCallSuper
  void dispose() {
    assert(_debugLifecycleState == _StateLifecycle.ready);
    assert(() { _debugLifecycleState = _StateLifecycle.defunct; return true; }());
  }

  
  @protected
  Widget build(BuildContext context);

  
  @protected
  @mustCallSuper
  void didChangeDependencies() { }
}
复制代码

State的生命周期

digraph {
    rankdir=TB;

    createState [shape=box label="createState" fillcolor=lightblue style=filled];
    
    BuildContext [shape=box label="关联BuildContext及widget" fillcolor=lightblue style=filled];

    initState [shape=box fillcolor=lightgray style=filled];

    didChangeDependencies [shape=box fillcolor=lightgray style=filled];

    build [shape=box fillcolor=lightgray style=filled];

    renderTree [label="render tree"];
    removeWidget [label="remove widget"];

    didUpdateWidget [shape=box fillcolor=lightgray style=filled];

    didChangeDependencies_ [shape=box label="didChangeDependencies" fillcolor=lightgray style=filled];
    build_ [shape=box label="build" fillcolor=lightgray style=filled];

    createState -> BuildContext -> initState -> didChangeDependencies -> build;

    build -> renderTree;

    setState [shape=box fillcolor=lightgray style=filled];
    {rank=same; build; setState;}
    setState -> build; 

    renderTree [shape=MRecord fillcolor=lightblue style=filled];
    removeWidget [shape=rect fillcolor=lightblue style=filled];
    reinsert [shape=rect style=rounded];

    renderTree -> didUpdateWidget -> build_;

    renderTree -> didChangeDependencies_ -> build_;

    build_:w -> renderTree:w;

    {rank=same; renderTree; removeWidget;}
    renderTree:e -> removeWidget:w;

    deactivate [shape=box fillcolor=lightgray style=filled];
    dispose [shape=box fillcolor=lightgray style=filled];

    removeWidget -> deactivate;

    deactivate:s -> dispose;

    deactivate -> reinsert;

    reinsert -> build_;

    dispose -> over;

    over [shape=box fillcolor=lightblue style=filled];
    reinsert [shape=box fillcolor=lightblue style=filled];

    {rank=same; dispose; build_}
    {rank=same; didUpdateWidget; didChangeDependencies_;}
}
复制代码
  • 框架调用StatefulWidget.createState创建State对象
  • 新创建的State对象关联到BuildContext。这种关联是永久的:State对象永远不会改变它的BuildContext。尽管如此,BuildContext可以带着它的子树移到树的其它位置。这时,State对象被认为是挂裁的mounted
  • 框架调用initStateState子类应该重写initState执行一次性初始化,初始化依赖于BuildContext或者该widget。BuildContext和该widget分别作为Statecontextwidget的属性,当initState被调用后,它们是可用的。
  • 框架调用didChangeDependencies。子类应该重写didChangeDependencies执行涉及InheritedWidget的初始化。如果BuildContext.inheritFromWidgetOfExactType被调用了,随后当inherited widgets发生改变或者该widget在树中发生转移,didChangeDependencies方法被会被再次调用。
  • 这时State对象已经完全初始化了。框架可能多次调用build方法获得该子树对UI的描述。State对象可以通过调用setState自发地请求重构子树,这标志着子树的内部状态发生改变,并可能会影响到UI。
  • 在这段时间,父widget可能重构并请求更新树中的这个位置,该更新显示一个相同runtimeTypeWidget.key的新widget。当这种情况发生后,框架会更新State.widget属性指向这个新的widget,并使用先前的widget调用didUpdateWidget方法。State对象应该重写didUpdateWidget响应与它关联的widget的改变(比如开始隐式动画)。调用完didUpdateWidget后框架总是会调用build方法,所在在didUpdateWidget中调用setState是多余的。
  • 在开发阶段,当热重载发生后,reassemble方法会被调用。这提供了重置数据的机会,这些数据是由initState方法准备好的。
  • 如果包令State对象的子树从树中移除(比如你widget构建了不周runtimeType或者Widget.key的子widget),框架将会调用deactivate方法。子类应该重写该方法清理State对象与树中其它element的引用关系(比如为祖先结点提供了指向后代RenderObject的指针)。
  • 此时,框架可能将子树重新插入到树的其它部分。如果这种情况发生,框架将确保调用build方法使State对象有机会适配树中的新位置。如果框架确实要重插入该子树,框架将在动画帧结束之前执行插入,这时子树已经从树中移除了。因此,State对象可以延迟释放资源,直到dispose被调用后再释放资源(从1.7.8源码可以看出,只有引用GlobalKey的widget有可能被重新插入)。
  • 如果框架在当前动画帧结束之前没有重新插入该子树,框架将会调用dispose,这意昧着State对象永远也不会重新构建。子类应该重写该方法释放持有的资源(比如结束作何活动的动画)。
  • 在框架调用dispose后,State对象是被卸载的,并且mounted属性是false。这时如果再调用setState是错误的(这一步要特别注意,实际开发中,大部分异常都与此有关,当异步调用返回后,通常需要改变状态,此时一定要判断一下State对象的状态,否则会抛出异常)。这时生命周期走到了最终:当State对象被销毁后没有任何办法可以重新挂载。

从框架的源码分析:

class StatefulElement extends ComponentElement {
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    _state._element = this;
    _state._widget = widget;
    _StateLifecycle.created);
  }
}
复制代码

从StatefulElement源码可以看出,创建StatefulElement时,一并会调用StatefulWidget.createState创建State对象,并关联Buildcontext及widget。 _state._element = this;该行代码关联BuildContext,并且此关联关系是不变的,在State对象的生命周期中,BuildContext是再不可变的,Flutter当中,BuildContext是由Element来现的。 _state._widget = widget;关联widget。

@override
void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    assert(_child == null);
    assert(_active);
    _firstBuild();
    assert(_child != null);
}

@override
void _firstBuild() {
...
try {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
    final dynamic debugCheckForReturnedFuture = _state.initState() as dynamic;
} finally {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}

_state.didChangeDependencies();

super._firstBuild();
}
复制代码

当Element通过mount方法被插入到Elemnt树中,会调用_firstBuild_firstBuild则依次调用了State.initStateState.didChangeDependencies


@override
Widget build() => widget.build(this);

void _firstBuild() {
    rebuild();
}

void rebuild() {
if (!_active || !_dirty)
    return;
...
performRebuild();
...
}

@override
void performRebuild() {
...
try {
    built = build();
    debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
    built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
} finally {
    _dirty = false;
}
...
复制代码

_firstBuild紧接着会调用rebuildrebuild调用performRebuildperformRebuild调用buildbuild方法则会调用State.build创建子树。

可见,State.createState,关联BuildContextwidgetState.initStateState.didChangeDependenciesState.build,在StatefulElement插入到树中时,是一气呵成依次调用的。

@protected
void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
}
复制代码

State.setState会马上同步执行传入的回调,并标记element需要重建。随后element会调用State.build更新子树。

@override
  void update(StatefulWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    final StatefulWidget oldWidget = _state._widget;
    _dirty = true;
    _state._widget = widget;
    try {
      final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
    } finally {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
    }
    rebuild();
}
复制代码

当新旧widget的runtimeTypekey是相等时,框架会调用Element.update,该方法会调用StatefulWidget.didUpdateWidget通知widget已经更新。

@override
void didChangeDependencies() {
    super.didChangeDependencies();
    _state.didChangeDependencies();
}
复制代码

StatefulWidget依赖的InheritedWidget改变后,Element收到didChangeDependencies时,会通知State,调用State.didChangeDependencies

同理,element的deactivatedispose方法都会通知到StatefulWidget

ProxyWidget

持有一个子widget。其它只有一个子widget类的基类

源码

abstract class ProxyWidget extends Widget {

  const ProxyWidget({ Key key, @required this.child }) : super(key: key);

  final Widget child;
}
复制代码

InheritedWidget

可以高效传递数据到后代结点。

使用BuildContext.inheritFromWidgetOfExactType可以取得最近的特定类型的inherit widget实例。

这也会导致调用它的widget在inherited widget发生改变后重构。

源码:

abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ Key key, Widget child })
    : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);


  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
复制代码

使用例子

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.inheritFromWidgetOfExactType(FrogColor) as FrogColor;
  }

  @override
  bool updateShouldNotify(FrogColor old) => color != old.color;
}

复制代码

按照惯例InheritedWidget会提供一个静态的of方法,该方法会调用BuildContext.inheritFromWidgetOfExactType。在域中如果没有这样的widget,这将允许类定义自己的返回逻辑。在上面的例子中,返回值有可能为空,在这种情况下,可以返回一个默认值。

of返回的数据也可以不是inherited widget,上面这个例子中,返回的是Color

有时,inherited widget是一个实现了其它类的细节,所以是私有的。of方法将由其它公开类提供。比如Theme实现了StatelessWidget构建私有的inherited widget;Theme.of方法通过BuildContext.inheritFromWidgetOfExactType寻找inherited widget并且返回。

InheritedWidget重建时,需要通知遗传此widget的其它widget重建,但有时并不需要通知。比如该widget持有的数据与旧的widget持有的相同,我们没有必要通知遗传widget重建。

框架通过调用InheritedWidget.updateShouldNotify来区别这种情况,调用该方法会传递旧的widget作为参数。旧的widget的runtimeType保证与该类相同。