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的runtimeType
及key
都是相等的,其所对应的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有StatelessWidget
及StatefullWidget
,这两者可以管理一组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);
}
复制代码
相比Widget
,StatelessWidget
提供了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
等。如果绘制图形效果需要多个Container
及Decoration
组成复杂的分层效果,那应该考虑使用单个CustomPaint
Widget。 - 尽可能使用
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包装起来,该GlobalKey
在StatefulWidget
生命周期里保持一致。(如果其它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
。 - 框架调用
initState
。State
子类应该重写initState
执行一次性初始化,初始化依赖于BuildContext
或者该widget。BuildContext
和该widget分别作为State
的context
及widget
的属性,当initState
被调用后,它们是可用的。 - 框架调用
didChangeDependencies
。子类应该重写didChangeDependencies
执行涉及InheritedWidget
的初始化。如果BuildContext.inheritFromWidgetOfExactType
被调用了,随后当inherited widgets发生改变或者该widget在树中发生转移,didChangeDependencies
方法被会被再次调用。 - 这时
State
对象已经完全初始化了。框架可能多次调用build
方法获得该子树对UI的描述。State
对象可以通过调用setState
自发地请求重构子树,这标志着子树的内部状态发生改变,并可能会影响到UI。 - 在这段时间,父widget可能重构并请求更新树中的这个位置,该更新显示一个相同
runtimeType
和Widget.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.initState
及State.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
紧接着会调用rebuild
,rebuild
调用performRebuild
,performRebuild
调用build
,build
方法则会调用State.build
创建子树。
可见,State.createState
,关联BuildContext
及widget
,State.initState
,State.didChangeDependencies
,State.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的runtimeType
及key
是相等时,框架会调用Element.update
,该方法会调用StatefulWidget.didUpdateWidget
通知widget已经更新。
@override
void didChangeDependencies() {
super.didChangeDependencies();
_state.didChangeDependencies();
}
复制代码
当StatefulWidget
依赖的InheritedWidget
改变后,Element收到didChangeDependencies
时,会通知State
,调用State.didChangeDependencies
。
同理,element的deactivate
及dispose
方法都会通知到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
保证与该类相同。