目录介绍
01.Widget基础概念
- 1.1 Widget概念
- 1.2 Widget骨架
- 1.3 Widget源码
- 1.4 Widget不可变
- 02.StatelessWidget源码
- 03.StatefulWidget源码
- 04.InheritedWidget源码
- 05.Context是什么作用
01.Widget基础概念
1.1 Widget概念
在Flutter中几乎所有的对象都是一个Widget。
- 与原生开发中“控件”不同的是,Flutter中的Widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的
GestureDetector
widget、用于APP主题数据传递的Theme
等等,而原生开发中的控件通常只是指UI元素。 - 在描述UI元素时可能会用到“控件”、“组件”这样的概念,读者心里需要知道他们就是widget,只是在不同场景的不同表述而已。
- 由于Flutter主要就是用于构建用户界面的,所以,在大多数时候,可以认为widget就是一个控件,不必纠结于概念。
- 与原生开发中“控件”不同的是,Flutter中的Widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的
1.2 Widget骨架
Widget 的骨架
- 常用的 StatefulWidget、StatelessWidget,再加上 (InheritedWidget) 或 ProxyWidget 和 RenderObjectWidget 都继承于 Widget 基类,他们整体构成了 Widget 的骨架。
有状态 和 无状态
- StatelessWidget 无状态的 Widget,常见的子类如 Text、Container。
- StatefulWidget 有状态的 Widget,常用的子类有 Image、Navigator。
- ProxyWidget 为代理 Widget,可以快速追溯父节点,通常用来做数据共享,常见的子类 InheritedWidget,各种状态管理框架,如 provider 等正是基于它实现。
- 什么叫做“状态”?Widget 在 Flutter 架构下设计为不可变的,通常情况下每一帧都会重新构建一个新的 Widget 对象,而无法知道之前的状态。StatefulWidget 通过关联一个 State 对象实现状态的保存。
1.3 Widget源码
Widget源码如下所示
abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; @protected @factory Element createElement(); @override String toStringShort() { final String type = objectRuntimeType(this, 'Widget'); return key == null ? type : '$type-$key'; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense; } @override @nonVirtual bool operator ==(Object other) => super == other; @override @nonVirtual int get hashCode => super.hashCode; static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } static int _debugConcreteSubtype(Widget widget) { return widget is StatefulWidget ? 1 : widget is StatelessWidget ? 2 : 0; } }
主要方法和属性介绍
Widget
类继承自DiagnosticableTree
,DiagnosticableTree
即“诊断树”,主要作用是提供调试信息。Key
: 这个key
属性类似于React/Vue中的key
,主要的作用是决定是否在下一次build
时复用旧的widget,决定的条件在canUpdate()
方法中。createElement()
:正如所述“一个Widget可以对应一个Element
”;Flutter Framework在构建UI树时,会先调用此方法生成对应节点的Element
对象。此方法是Flutter Framework隐式调用的,在我们开发过程中基本不会调用到。debugFillProperties(...)
复写父类的方法,主要是设置诊断树的一些特性。canUpdate(...)
是一个静态方法,它主要用于在Widget树重新build
时复用旧的widget,其实具体来说,应该是:是否用新的Widget对象去更新旧UI树上所对应的Element
对象的配置;通过其源码我们可以看到,只要newWidget
与oldWidget
的runtimeType
和key
同时相等时就会用newWidget
去更新Element
对象的配置,否则就会创建新的Element
。
核心方法createElement()
Widget
类本身是一个抽象类,其中最核心的就是定义了createElement()
接口。- 在Flutter开发中,我们一般都不用直接继承
Widget
类来实现一个新组件,相反,我们通常会通过继承StatelessWidget
或StatefulWidget
来间接继承Widget
类来实现。 StatelessWidget
和StatefulWidget
都是直接继承自Widget
类,而这两个类也正是Flutter中非常重要的两个抽象类,它们引入了两种Widget模型。
核心方法canUpdate()
实际上就是比较两个 widget 的 runtimeType 和 key 是否相同。
- runtimeType 也就是类型,如果新老 widget 的类型都变了,显然需要重新创建 Element。
- key Flutter 中另一个核心的概念,key 的存在影响了 widget 的更新、复用流程,这里先不展开。
- 默认情况下 widget 创建时不需传入 key,因此更多情况下只需要比较二者的类型,如果类型一样,那么当前节点的 Element 不需要重建,接下来继续调用 child.update 更新子树。
1.4 Widget不可变
Widget 是一个很重要的概念,但是Widget有一个更重重要的特性,就是Widget是immutable(不可变的),这是什么意思?
- 拿 Opacity 为例给讲解,讲解之前先看一下Opacity的继承关系。(在讲源码之前我们先看一下Opacity的职责是什么,Opacity是一个能让他的孩子透明的组件,很简单也很容易理解。)
- Opacity继承自SingleChildRenderObjectWidget,这类只包含了一个child的Widget,它继承自RenderObjectWidget,RenderObjectWidget继承自Widget。
class Opacity extends SingleChildRenderObjectWidget { } abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { } abstract class RenderObjectWidget extends Widget { }
然后看一下 Opacity 简单源码
class Opacity extends SingleChildRenderObjectWidget { const Opacity({ Key key, @required this.opacity, Widget child, }) : super(key: key, child: child); final double opacity;//注释1 @override RenderOpacity createRenderObject(BuildContext context) {//注释2 return RenderOpacity( opacity: opacity ); } @override void updateRenderObject(BuildContext context, RenderOpacity renderObject) { renderObject ..opacity = opacity } }
- 在注释1处声明了一个属性,这属性是final,也就除了构造函数能给这个属性赋值之外,没有其他的办法让这个值进行改变。那我们想改变这个值怎么办,唯一的办法就是创建一个新的Opacity。
02.StatelessWidget源码
StatelessWidget源码如下所示
abstract class StatelessWidget extends Widget { const StatelessWidget({ Key key }) : super(key: key); @override StatelessElement createElement() => StatelessElement(this); @protected Widget build(BuildContext context); }
StatelessWidget
相对比较简单,它继承自Widget
类,重写了createElement()
方法。StatelessElement
间接继承自Element
类,与StatelessWidget
相对应(作为其配置数据)。
StatelessWidget
用于不需要维护状态的场景。- 它通常在
build
方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。
- 它通常在
03.StatefulWidget源码
StatefulWidget源码如下所示
abstract class StatefulWidget extends Widget { const StatefulWidget({ Key key }) : super(key: key); @override StatefulElement createElement() => StatefulElement(this); @protected @factory State createState(); }
StatefulElement
间接继承自Element
类,与StatefulWidget相对应(作为其配置数据)。StatefulElement
中可能会多次调用createState()
来创建状态(State)对象。
createState()
用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。- 例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个
StatefulElement
对应一个State实例。
- 例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个
理解树的概念
- 在不同的场景可能指不同的意思,在说“widget树”时它可以指widget结构树,但由于widget与Element有对应关系(一可能对多)。
- 在有些场景(Flutter的SDK文档中)也代指“UI树”的意思。
- 而在stateful widget中,State对象也和
StatefulElement
具有对应关系(一对一),所以在Flutter的SDK文档中,可以经常看到“从树中移除State对象”或“插入State对象到树中”这样的描述。 - 其实,无论哪种描述,其意思都是在描述“一棵构成用户界面的节点元素的树”,如果没有特别说明,都可抽象的认为它是“一棵构成用户界面的节点元素的树”。
04.InheritedWidget源码
4.1 InheritedWidget源码分析
InheritedWidget源码如下所示
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); }
如果想自己实现一个类似主题变更后,更新相应 UI 的功能应该怎么做?
- 大致思路应该就是一个观察者模式,凡是使用的主题数据的地方,需要向 主题中心 注册一个观察者,当主题数据发生 改变 时,主题中心依次通知各个观察者进行 UI 更新。
- 这里有个问题需要解决,如何定义 数据改变 ?事实上,数据是否改变是由业务方决定的,因此这里需要抽象出相应接口,来看 InheritedWidget 结构。
核心就是 updateShouldNotify 方法
- 入参为原始的 widget,返回值为布尔值,业务方需要实现此方法,判断是否需要将变化通知到各个观察者。
4.2 注册和通知流程
举一个常见例子
- 比如,app设置了theme主题,当修改了theme主题颜色,是怎么修改全局所有页面的theme状态的呢?这个就用到了注册和通知的功能,接着往下看:
注册流程
- 假设 StudyWidget 是我们业务侧的 Widget,其内部使用了 Theme.of(context) 方法获取任意主题信息后,会经一系列调用,最终将这个 context——StudyWidget 对应的 Element 对象,注册到 InheritedElement的成员变量 Map<Element, Object> _dependents 中。
- 另外需要注意,之所以在第二步中,可以找到父 InheritedElement,是因为在 Element 的 mount 过程中,会将父 Widget 中保存的 Map<Type, InheritedElement> _inheritedWidgets 集合,依次传递给子 Widget。如果自身也是 InheritedElement 也会添加到这个集合中。
- 当我们使用 MaterialApp 或 CupertinoApp 作为根节点时,其内部已经帮我们封装了一个 Theme Widget,因此不需要我们额外的套一层作为注册了。
通知流程
- 当父 InheritedWidget 发生状态改变时,最终会调用到 InheritedElement 的 update 方法,我们以此作为通知的起点。
- 可以看到,流程最终会将依赖的 Element 标脏,在下一帧重绘时将会更新对应 Widget 的状态。至此,InheritedWidget 整体的注册、通知流程结束。
05.Context是什么作用
什么是Context
build
方法有一个context
参数,它是BuildContext
类的一个实例,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。- 实际上,
context
是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法。
下面是在子树中获取父级widget的一个示例:
class ContextRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Context测试"), ), body: Container( child: Builder(builder: (context) { // 在Widget树中向上查找最近的父级`Scaffold` widget Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>(); // 直接返回 AppBar的title, 此处实际上是Text("Context测试") return (scaffold.appBar as AppBar).title; }), ), ); } }