通过一系列文章记录一下学习 Flutter 的过程,总结一下相关知识点。
Widget 是什么
Everything is a Widget in Flutter
在 Flutter 中,一切皆 Widget,为什么这么说呢?它不像我们 iOS 和 Android 开发中 View 的概念,不仅可以标识 UI 元素,也可以标识一些功能性的组件,总之一切与图形构建相关的东西都是 Widget,比如:
- 一个 UI 结构元素(如:Button、Text、GestureDetector等)
- 一个 UI 样式元素(如:color、font、Theme等)
- 一个 UI 布局元素(如:Row、Padding等)
等等这些都称之为 Widget,是一种合理的命名抽象。
Widget 的功能是 "描述一个 UI 元素的配置信息",注意是"配置信息",Flutter 只是通过 Widget 来接收各种参数, 拿 Text 来说比如:文本内容、对齐方式、颜色样式等等,然后再通过一系列的转换和渲染操作,最终在屏幕上显示出来的元素并不简单的是我们书写的 Widget。
在 Flutter 中通过 Widget 构建 UI 的过程中,有一些显著的特点:
- 声明式 UI
声明式编程:告诉"机器" 你想要什么(what)即可,机器自己计算如何去做(how)
命令式编程:告诉"机器" 如何去做(how),无论你想要什么(what),都需要你一步步的告诉机器。
相比于传统的命令式编程,声明式编程的优势很显著,不仅可以提高我们的开发效率和代码质量,减少代码出错的可能性,同时也能够促进程序的复用性和可维护性。
- 不可变性
Flutter 中所有Widget都是不可变的(@immutable),并且其内部成员都是不可变的(final),对于需要变化的部分可通过 Widget-State ( 如:StatefuleWidget)的方式实现;
- 组合大于继承
Widget 设计遵循组合大于继承这一优秀的设计理念,通过将多个功能相对单一的Widget组合起来得到功能相对复杂的Widget。
Widget 类型
我们先从全局的角度看看 Widget 的种类有哪些,然后在具体的看下每种 Wdiget 的作用以及我们该如何使用他们。
如图所示,各种 Widget 之间的继承关系一目了然,带背景色的 Widget 都是抽象类,无法直接使用,都是在各个子类中实现的对应的功能。按照功能划分 Widget 可分为 3 大类
-
组合类(Component Widget)
我们开发中用到最多的就是这一类型的 Widget,这类 Widget 都是直接或者间接的继承自 StatelessWidget 或 StatefulWidget,通过组合一些功能相对比较单一的 Widget 来得到满足我们需求的比较复杂的 Widget。
StatelessWidget 无状态的 Widget,常见的子类有:Text、Container 等。
StatefulWidget 有状态的 Widget,常见的子类有:Image、CheckBoxs 等。
-
代理类(Proxy Widget)
ProxyWidget 总共有两个子类 InheritedWidget 和 ParentDataWidget
这类 Wdiget 可以快速追溯父节点,主要的作用就是为其 Child Widget 提供一些中间的附加功能。比如:InheritedWidget 常被用于做数据共享,Theme/ThemeData 就是通过它来实现的数据共享,以及 Flutter 中知名的状态管理框架 Provider 也是通过它实现的。
-
渲染类(Renderer Widget)
RenderObjectWidget 是Flutter 中最核心的 Widget 类型,只有它才会直接参与最终的 layout 与 paint 流程。无论是 Component Widget 还是 Proxy Widget 最终都会映射到 Renderer Widget 上。
Widget 源码
下面我们先来看一下 Widget 类的源码,主要介绍一下核心的属性和方法
dart
@immutable // 不可变的
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key? key;
@protected
@factory
Element createElement();
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
...
}
-
@immutable
表示 Widget 是不可变的,同时也限制了 Widget 的属性(配置信息)是不可变的,因为如果属性发生变化会导致 Wdiget 树的重新构建,即会用新的 Widget 实例替换旧的 Widget 实例,所以一旦属性变了自身就会被替换,那么允许属性变化是没有意义的,属性必须是 final 的。
-
Key
在同一父节点下,用作兄弟节点间的唯一标识,主要作用是决定在下一次 build 时是否复用旧的 widget ,还是重建新的 widget,用在了 canUpdate 方法中。
-
createElement()
每个 widget 都会有一个对应的 Element,就是由该方法创建的。Flutter 构建 UI 树时,会先调用此方法生成对应节点的 Element 对象。Widget 的子类都会重写此方法,从而生成不同类型的 Element 对象。
Flutter 中的 Widget 是以树的结构进行渲染的,实际上 Element 才是树的节点,所有对 Widget 的添加、删除等操作,实际上是对 Element 进行操作的,Widget 就是 Element 的配置项而已。
-
canUpdate(...)
是一个静态方法,主要用于当 widget 树重新 build 时,判断新旧 widget 的 runtimeType 和 key 是否相等,决定是否用 new widget 对象去更新旧 UI 树上的 Element 配置。如果两个值都相等,则会用new widget 去更新 Element 对象配置,若不相等,则会创建新的 Element 对象。
在我们日常开发中,打交道最多的应该是 StatelessWidget 和 StatefulWidget 这两个类,他们都是继承自 Widget 的抽象类。StatelessWidget 和 StatefulWidget 这两个类他们之间的相同点是都属于组合型widget,区别是无状态与有状态之分。下面我们通过源码来具体分析一下。
StatelessWidget
无状态的 - 组合型Widget。
所谓无状态,意思是不需要管理内部状态,通常在其 build 方法中描述组合 UI 的层级结构,在其生命周期内是不可保存和修改状态的。
源码分析
dart
abstract class StatelessWidget extends Widget {
/// Initializes [key] for subclasses.
const StatelessWidget({ Key key }) : super(key: key);
/// Creates a [StatelessElement] to manage this widget's location in the tree.
///
/// It is uncommon for subclasses to override this method.
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
-
abstract
该类是一个抽象类,继承自它的子类只需要实现 build() 方法即可。
-
StatelessElement createElement()
子类一般无需重写该方法,已经给我们创建好了一个 StatelessElement 类型的 Element,会将当前 widget 对象(this)当参数传进去,子类对应的 Element 类型也为StatelessElement。
-
Widget build(BuildContext context)
build 方法属于 Flutter 体系中的核心方法之一,子类需要重写该方法,通过声明式 UI
的形式来描述 widget 的层级结构以及样式信息。该方法会被Flutter 系统自动调用,我们开发者重写即可,该方法被调用的情况有:
- widget 首次被加入到 Widget Tree 中(准确的说是其对应的 Element 被加入到 Element Tree 中,即 Element 首次被挂载时);
- Parent Widget 修改了配置信息;
- 该 Widget 依赖的 Inherited Widget 发生变化时;
-
BuildContext
build 方法的参数是一个 BuildContext 类型的实例,表示当前 widget 在 widget 树的上下文。可通过 context 从当前 widget 开始向上遍历 widget 树,以及可能够按照 widget 类型向上查找父级 widget。
dart
// 在 widget 树中向上查找最近的父级`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title
var title = (scaffold.appBar as AppBar).title;
优化建议
由于 Parent Widget 或依赖的 Inherited Widget 频繁变化时,导致 build 方法也会被频繁调用,因此对 build 方法的性能提升就显的尤为重要。Flutter 官方给出了几点建议:
- 减少不必要的中间节点,即减少 UI 层级结构。比如:为了实现某种复杂的UI 效果,不一定通过组合多个 Container ,在配置 Decoration 来实现,通过 CustomPaint 自定义或许是更好的选择;
- 尽量使用 const Widget,为 Widget 提供 const 构造方法,可参考: Dart Constant Constructors
- 尽量减少 rebuild 的范围,比如:某个 Widget 的 build 需要频繁执行,可以分析出哪些 Widget 是真正需要变化的部分,从而封装成更小的独立的 Widget,并尽量将该 Widget 推向树的叶子节点,从而减小 rebuild 的范围。
StatefulWidget
有状态的- 组合型Widget。
什么叫做有"状态"呢?通过 Widget 源码我们知道,Widge 在 Flutter 中是不可变的,通常情况下每次刷新都会重新构建一个新的 Widget 对象,而无法知道保留之前的状态。 StatefulWidget 通过关联一个 State 对象,实现了状态的保存。
比如:一个 button 具有 normal、highlighted 两种状态,当和用于交互时会展现不同的样式,那么样式更新的过程其实就是状态更新的过程。
下面通过源码来看看 StatefulWidget 有哪些特别之处。
源码分析
dart
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
通过观察可发现与 StatelessWidget 不同之处有:
StatelessWidget
- 默认创建了 StatelessElement 类型的 Element
- 需要子类实现 build 方法,返回 Widget 对象
StatefulWidget
-
默认创建了 StatefulElement 类型的 Element
-
需要子类实现 createState 方法,返回 State 对象。一个 StatefulWidget 类会对应一个 State 类,State 就是 Widget 要维护的状态的实现。
State
状态就是保存在 State 这个抽象类中,通过一张类图来帮助我们梳理一下 State 内部结构:
State 中的属性:
- widget
内部持有与该 State 关联的 widget 实例,Flutter 框架动态关联设置的。
注意:这种关联关系不是永久的,在应用生命周期中,UI 树上的某个节点的 widget 实例会在重新构建时会发生变化,但是 State 对象只会在第一次插入到树中被创建,当重新构建时,如果 widget 被修改了,系统会自动重新关联 State 的 widget 实例。
- context
作用同 StatelessWidget 的BuildContext。这个 context 对象 '==' _element。
State 生命周期
Flutter 中说到的生命周期,是指有状态的 Widget 的生命周期,对于无状态的 Widget 的生命周期只有一个 build 过程,也就只会渲染一次。
有状态的 Widget 的生命周期如下:
状态说明:
- createElement()
首先,当一个 StatefulWidget 需要被显示到页面上时,会创建一个 StatefulElement , 在 StatefulElement 的构造函数中会调用 createState()
- createState()
创建 State 对象,同时将创建的 element 对象赋值给 state 的_element 对象,此时 element 就挂在到了 Element Tree 中,state 就处于 mounted 状态。
- initState()
在element 挂在的过程中紧接着会调用该方法,并且只会被调用一次。我们可以重写该方法,执行一些初始化操作。(注意:此时已经可以引用 context 、widget 属性了)
- didChangeDependencies()
首先,当 initState 调用结束后也会调用一次。并且,当 state 依赖的对象状态发生变化时,该方法也会被调用。如:父级 widget 中包含了InheritedWidget,它的状态发生了变化,那么InheritedWidget 的子 Widget 的 didChangeDependencies 方法也会被调用。
子类一般很少重写该方法,除非有非常耗时的不适合在 build 方法中进行的操作。
- build()
主要用于构建 Widget 子树的,会在以下场景被调用:
- 调用 initState() 之后
- 调用 didChangeDependencies 之后
- 调用 didUpdateWidget() 之后
- 调用 setState() 之后
- 当 State 对象从树中一个位置移除后,又被插入到其他位置之后
- didUpdateWidget()
当 widget 重新构建时,Flutter 框架会调用 widget.canUpdate 方法来检测 Widget 树中同一位置的新旧节点,判断是否需要更新,如果 widget.canUpdate 返回 true表示需要更新,那么则会触发此回调。
- deactivate()
在Widget 树更新过程中,任何节点都有被移除的可能,State 也会随之移除,当 State 对象从树中被移除时候,会调用此方法。
有些场景下,会被重新插入到树中新的位置,此时会重新走 build 方法。如果没有被插入到新的位置,则会调用 dispose 方法进行销毁。
- dispose()
销毁方法,当前帧动画结束时扔未被插入到新的节点,则表示 State 对象从树中被永久移除,该方法会被调用,State 生命周期随之结束。
子类通常重写改方法,在这里执行一些资源释放的操作。
setState 方法介绍
上面在介绍 State 生命周期的过程中,我们开发中一般通过调用 setState() 方法来刷新 UI,此时 Flutter 框架会触发 build 方法重新构建 Widget 树,setState 工作机制流程基本如下:
那么 setState() 方法具体做了什么呢,下面我们来看一下源码的具体的实现"
dart
@protected
void setState(VoidCallback fn) {
//fn 不能为空
assert(fn != null);
assert(() {
//销毁后调用会报错
if (_debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() called after dispose(): $this'),
]);
}
//初始化但未挂载状态调用会报错
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() called in constructor: $this'),
]);
}
return true;
}());
//执行回调 fn
final Object? result = fn() as dynamic;
assert(() {
//异步方法会报错
if (result is Future) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('setState() callback argument returned a Future.'),
]);
}
return true;
}());
//标记需要刷新
_element!.markNeedsBuild();
}
根据源码以及相关注释,使用 setState() 方法时候有几点需要注意:
-
在 State 的 dispose() 方法后不能调用 setState(注意:程序中有些异步或延时的方法中,是否调用了);
-
在 State 的构造方法中不能调用 setState;
-
setState 方法的回调函数 fn 不能是异步的 (返回值为 Future 类型),因为系统需要回调产生的新状态去刷新 UI,如果回调里有很长的耗时任务,那么后续刷新的代码就会延迟执行,从而导致 build 也就延迟了;
-
最终 UI 的是通过 _element!.markNeedsBuild() 标记需要刷新,会等到下一个 Vsync 信号到来时才进行重新 build 和渲染。
ParentDataWidget
ParentDataWidget 继承自 ProxyWidget ,是 Proxy 型 Widget,主要用于配置子 Widget 的 RenderObject 提供 ParentData 数据,比如:布局、绘制等。例如,Stack 使用 Positoned(继承 ParentDataWidget)来定位每个子 widget。
首先看一下源码定义:
scala
abstract class ParentDataWidget<T extends ParentData> extends ProxyWidget {
const ParentDataWidget({ super.key, required super.child });
@override
ParentDataElement<T> createElement() => ParentDataElement<T>(this);
void applyParentData(RenderObject renderObject);
}
-
ParentDataWidget 本身也是抽象类,继承自 ProxyWidget
-
在定义上使用了泛型
-
需要子类去实现 applyParentData 方法,子类在这个方法中设置 renderObject.parentData 数据
ParentData 是什么?
ParentData 是 parent RenderObject 在布局 child RenderObject 时所用到的布局定位信息,其存储在子 widget 上。比如:子类对父类布局算法的输入参数,或子类相对其他子类的位置信息。
例子
配合 Stack 使用的 Positioned(继承 ParentDataWidget), 源码如下
dart
class Positioned extends ParentDataWidget<StackParentData> {
...
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is StackParentData);
final StackParentData parentData = renderObject.parentData! as StackParentData;
bool needsLayout = false;
if (parentData.left != left) {
parentData.left = left;
needsLayout = true;
}
if (parentData.top != top) {
parentData.top = top;
needsLayout = true;
}
......
if (needsLayout) {
final AbstractNode? targetParent = renderObject.parent;
if (targetParent is RenderObject) {
targetParent.markNeedsLayout();
}
}
}
...
}
从源码中可以看出,Positioned 会将自己的属性赋值给 renderObject.parentData,也就是 StackParentData,并且最后对 parent RenderObject 调用 markNeedsLayout,从而重新 layout,毕竟修改了布局信息。
总之,ParentDataWidget 就是用来配置 RenderObject 的 ParentData 数据的, ParentDataWidget 并不是任何 Widget 都能继承使用的,并配置指定的 ParentData 才行。
InheritedWidget
InheritedWidget 同样继承自 ProxyWidget ,是 Proxy 型 Widget,它的主要功能是用于在 Widget 树上向下传递数据(注意是从上到下)。比如我们在根 Widget 中通过 InheritiedWidget 共享一个数据,那么在所有子 Widget 中都可以获取这个共享数据,如 Flutter 框架正式通过 InheritedWidget 实现的共享主题(Theme)、当前语言环境(Local)、MediaQuery 等。
源码:
scala
abstract class InheritedWidget extends ProxyWidget {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const InheritedWidget({ super.key, required super.child });
@override
InheritedElement createElement() => InheritedElement(this);
/// Whether the framework should notify widgets that inherit from this widget.
///
@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
-
默认创建了 InheritedElement 类型的 Element,一般子类无需重写
-
子类重写 updateShouldNotify 方法,当 InheritedWidget 数据发生变化,判断那些依赖它的子 Widget 是否需要进行 rebuild。
例子
看下我们经常上使用 MediaQuery 的源码,如下:
dart
class MediaQuery extends InheritedWidget {
MediaQuery({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
final int data; //需要在子树中共享的数据
//定义一个便捷方法,方便子树中的widget获取共享数据
static MediaQueryData? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
}
//该回调决定当data发生变化时,是否通知子树中依赖data的Widget重新build
@override
bool updateShouldNotify(ShareDataWidget old) {
return old.data != data;
}
}
- of 方法
通常为了使用方便,提供了 of 方法,内部调用了 BuildContext 的dependOnInheritedWidgetOfExactType 方法可以获取最近的 InheritedWidget,
of 方法可以直接返回 Widget, 也可以返回 data 数据。
- updateShouldNotify
判断新旧 Widget 的 data 不相等时,才去更新依赖它的子 Widget。
didChangeDependencies
在介绍 StatefulWidgt 时, 我们提到了 State 的生命周期中有一个 didChangeDependencies 方法回调,该方法会在'依赖'发生变化时被 Flutter 框架调用,而这个'依赖'就是指子 Widget 中使用了父 Widget 中InheritedWidget 中的数据。如果使用了,则代表有子 Widget 依赖,没有使用,则代表没有依赖。比如:当主题(Theme)、本地语言(Locale)等发生变化时,依赖他们的子 Widget 的 didChangeDependencies 会自动被调用。
为什么会被调用?
首先,对依赖数据是通过 of 方法获取的, of 方法内部又调用 BuildContext 的dependOnInheritedWidgetOfExactType 方法,我们追踪源码看下
dart
static MediaQueryData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
}
dependOnInheritedWidgetOfExactType,方法内部有调用了dependOnInheritedElement
dart
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
dependOnInheritedElement
dart
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget as InheritedWidget;
}
看到这里,我们就明白了,最终在 dependOnInheritedElement 方法内部,所有对 InheritedWidget 中数据有依赖的子 Widget 注册了依赖关系,之后当 InheritedWidget 中数据发生变化时,便会更新依赖它的子Widget,也就是调用这些子 Widget 的 didChangeDependencies() 方法和build() 方法。
RenderObjectWidget
RenderObject 是什么?
RenderObject 对象作为 Render Tree 中的节点, Flutter 最终的布局、渲染的算法都是通过对应的 RenderObject 对象来实现的,比如:Stack(层叠布局)对应的 RenderObject 对象类型就是 RenderStack,层叠布局的实现就在 RenderStack 中。
RenderObjectWidget
RenderObjectWidget 属于布局类组件,此类组件都会包含一个或多个子组件,不同类型的布局类组件对子组件排列(layout)方式不同。根据子节点个数不同,共有三个子类:
- LeafRenderObjectWidget
作为叶子节点,只提供 paint 能力,如:RawImage。我们常用的 Image 组件内部的绘制工作全由 RawImage 实现。
- SingleChildRenderObjectWidget
仅接收单个 child 的 widget,如:Opacity。用来修改 child 透明度。
- MultiChildRenderObjectWidget
可接受多个 child 的 widget,提供 layout 能力,如:Stack 做层叠布局,Flex 做线性布局(Row、Column)。
我们来看下 RenderObjectWidget 的源码定义:
dart
abstract class RenderObjectWidget extends Widget {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RenderObjectWidget({ super.key });
/// RenderObjectWidgets always inflate to a [RenderObjectElement] subclass.
@override
@factory
RenderObjectElement createElement();
@protected
@factory
RenderObject createRenderObject(BuildContext context);
@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
@protected
void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
- createElement()
RenderObjectWidget 对应的 Element 类型是 RenderObjectElement,但是 RenderObjectElement 也是抽象类,该方法需要被子类重写
- createRenderObject()
创建 Render Widget 对应的 RenderObject 对象,创建时机是在 RenderObjectElement.mount 方法中,即 Element 被挂载到树上时调用,即 Element 被挂载过程中同时构建了 Render Tree。
- updateRenderObject()
更新 RenderObject,当 Widget 更新后,会修改对应的 RenderObject。
- didUnmountRenderObject()
RenderObject 从 Render Tree 上被移除时调用该方法。
以上,简要介绍了 Flutter 中 Widget 的几个大类,以及对应的源码定义,我们开发过程使用到的各种 Widget 基本都是继承自这些基类,感兴趣可以直接去看下他们的实现源码。
总结:
- Widget 本质上是 UI 的配置信息
- Widget 从功能上可以分为 3 大类:Component Widget、Proxy Widget 、Render Widget
- Widget 与 Element 是一一对应的,Widget 中会提供 Element 的创建方法 createElement()
- 只有 Render Widget 才会参与最终UI 的布局、渲染,提供 RenderObject 的创建方法 createRenderObject()