目录介绍
- 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就是一个控件,不必纠结于概念。
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源码
03.StatefulWidget源码
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 _dependents 中。
- 另外需要注意,之所以在第二步中,可以找到父 InheritedElement,是因为在 Element 的 mount 过程中,会将父 Widget 中保存的 Map _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();
// 直接返回 AppBar的title, 此处实际上是Text("Context测试")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
推荐:https://github.com/yangchong211/YCFlutterUtils