上一节介绍了dart的安装和使用,这一节让我们一起正式进入flutter的世界里。好的,话不多说,让我们一起开启flutter的探索之旅吧!
第一节:Flutter基本组件StatefulWidget和StatelessWidget
第一节,我们先一起来熟悉一下flutter中遍地的widget,了解一下它的结构和特点。
我们在开始学习flutter的时候,最先接触到的widget是StatefulWidget和StatelessWidget这两个基本组件,既然如此,那我们先来单独解析一下这两个组件的基本结构。
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
我们从上面它们的实现中可以了解到,代码量仅仅数行,其都继承自Widget组件类,并且都实现了来自基类Widget中的createElement()函数,创建出一个element对象。下面是基类Widget的代码结构:
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
基类Widget的代码量也是极其少的,它继承自DiagnosticableTree类,而DiagnosticableTree根据它名称含义,就是可诊断的树,无非就是产生一些有关widget配置数据的诊断信息(这个就不具体介绍了)。我们重点关注成员属性key和抽象函数createElement()还有一个静态函数canUpdate()。这也表明了只要实现了Widget类的组件都具备可配置key和创建element元素的特性,而静态函数canUpdate()主要用来比较两个widget组件的类型和配置的key是否都一致。
好,我们言归正传,继承了Widget组件的StatelessWidget和StatefulWidget的不同点是什么呢?就他们命名的含义来说:StatelessWidget意为无状态的组件;而StatefulWidget意为有状态的组件。无状态的组件StatelessWidget抽象出一个build()函数,该build函数需要传入一个BuildContext对象,并且返回一个Widget类型的组件。接触过flutter的童鞋们都知道build()是写flutter程序必需的函数,可以构建出我们想要的用户界面。那么,StatefulWidget的build()函数又在哪里?我们从StatefulWidget的代码结构可以了解到,虽然StatefulWidget没有build()函数,但是他有一个StatelessWidget不具备的函数createState(),该函数返回一个State对象,这就是StatelessWidget叫做有状态对象的实现之一,我们具体来看一下State类的代码实现:
//继承State对象的子类可以指明泛型T的具体类型,该T类型必须是StatefuleWidget组件类的子类型。
//通过对泛型T和内部成员属性的理解,可以得知以下两点信息:
//1.任何继承State的子类都拥有一个具备StatefuleWidget特性的组件。
//2.任何继承State的子类都拥有一个具备StatefuleElement特性的元素。
abstract class State<T extends StatefulWidget> extends Diagnosticable {
//可以保存一个widget组件对象
T get widget => _widget;
T _widget;
//可以保存一个BuildContext类型的上下文,
//然而这里的上下文实质上就是一个StatefulElement元素对象。
//这说明了任何StatefulElement都具有BuildContext特性。
BuildContext get context => _element;
StatefulElement _element;
//mounted的值跟元素是否为空相关
bool get mounted => _element != null;
//初始化状态
//@protected注释表明该函数可以被子类重写
//@mustCallSuper注释似乎在告诉子类,如果你想重写我,
//必须使用super.initState()调用到我的功能。
@protected
@mustCallSuper
void initState() {}
//当组件已经更新的时候会触发该函数的调用,
//这里可以拿着新组件widget和旧组件oldWidget
//处理一些你认为需要的逻辑(一般可以用来做数据优化)
@mustCallSuper
@protected
void didUpdateWidget(covariant T oldWidget) { }
//意为重新装配,
//可以实现你认为比较重要的处理过程,
//该函数的触发点在后面小节中可能会介绍
@protected
@mustCallSuper
void reassemble() { }
//设置状态
//1.首先会调用到传进来的函数fn()
//2.继而会调用到元素的markNeedsBuild()函数,该函数在后面章节中分析说明
@protected
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
//停用功能(比如移除元素的时候)
//为什么不直接销毁?增加此功能的目的可能是
//为了保留被移除的元素,也就是说元素可能都
//具有被复用的特性。
@protected
@mustCallSuper
void deactivate() { }
//丢弃功能
//该功能一旦调用,元素对象也就不能被再次复用了
@protected
@mustCallSuper
void dispose() { }
//当依赖发生改变的时候会调用到此函数,
//什么依赖会发生什么样的改变?猜测可能跟元素节点之间的依赖关系相关,
//在稍后的章节中一起来分析下。
@protected
@mustCallSuper
void didChangeDependencies() { }
//状态类中具备构建能力的函数。
//此函数为抽象函数,子类必须实现。
@protected
Widget build(BuildContext context);
}
从上面的代码中我们知道任何状态对象中都可以关联一个组件对象StatefulWidget和一个元素对象StatefulElement,而创建状态的责任由StatefulWidget对象负责,虽然StatefulWidget中没有和StatelessWidget类似的构建函数build(),但是在它的状态对象中存在,这样就相当于StatefulWidget也具备了和StatelessWidget相同的构建能力了。我们知道StatefuleWidget的状态和构建工作都保存在一个叫做State的对象中,而开发者只需要创建出一个State的子类即可轻松实现有状态的组件了,我们一般将状态数据和构造工作都放在同一个State状态对象内部,这很大的方便了我们在构建过程中对状态的管理。那么让状态对象State关联上StatefulWidget和StatefulElement这两个重要的类由谁负责呢?这个我们在介绍element一节中再继续讨论。
那么除了上面介绍的两种类型的widget组件,就没有其他widget组件了吗?答案是当然不止这两种。我们从上面的介绍中就可以了解到,StatelessWidget和StatefulWidget这两种组件的特性主要就两种:
1. 创建元素element。
2. 利用build()函数构建其他类型的Widget组件。
What?其他类型的Widget组件?是的,你没听错,还有其他不同于build特性的Widget组件。当我们想要创建自己定义的Widget组件的时候,实现的也就是这些其他类型的Widget组件了。
第二节:Flutter基本组件RenderObjectWidget和渲染对象RenderObject
第二节我们来了解一下flutter中的其他类型的组件之RenderObjectWidget组件(这里只介绍重要的组件)。前一小节我们简单介绍了一下flutter两种基本构建组件:无状态的组件StatelessWidget和有状态的组件StatefuleWidget,他们的主要作用是build()出我们可以在界面上显示的组件,当然build()中也可以存在无状态的组件StatelessWidget和有状态的组件StatefuleWidget,但是build()内部大部分都是其他类型的组件RenderObjectWidget,而RenderObjectWidget从字面上理解为渲染对象的组件,因为我们构建的目的最终只有一个,就是能在手机界面上显示出这些组件所描述的样子。那么RenderObjectWidget的作用就显而易见了,主要就是用来显示组件的,然而想要显示出界面,必然需要布局和绘制这两个基本步骤。我们一起来看看ReaderObjectWidget类的代码结构:
abstract class RenderObjectWidget extends Widget {
const RenderObjectWidget({ Key key }) : super(key: key);
@override
RenderObjectElement createElement();
@protected
RenderObject createRenderObject(BuildContext context);
@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
@protected
void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
我们从组件RenderObjectWidget类中的代码结构可以了解到,RenderObjectWidget组件和StatefuleWidget、StatelessWidget的共同点在于,都具备创建元素的能力createElement(),并且都拥有属性key。不同点在于RenderObjectWidget组件具备创建RenderObject对象的能力createRenderObject(),并且需要传入一个BuildContext类型的参数。其余两个函数从字面意义上理解无非就是还拥有更新和卸载RenderObject对象的能力。但是RenderObjectWidget类显示组件的能力怎么没看出来?这里就要我们深入到RenderObject类里面一探究竟,下面是RenderObject类的继承关系:
从上图可以看出RenderObject主要继承了AbstractNode和HitTestTarget两个类,而AbstractNode是抽象节点的含义,也就是说RenderObject具备抽象节点的相关特性;而HitTestTarget顾名思义为敲击测试目标,这就跟我们的点击事件相关了,也就是说任何RenderObject对象都具备可点击的特性。下面是AbstractNode类的代码结构:
//该类仅仅存在父节点内容,
//意味着子节点的实现逻辑交由其子类负责。
//covariant关键词代表子类可以缩小形参的类型。
class AbstractNode {
//意味着每个节点都具有深度值
int get depth => _depth;
int _depth = 0;
//提供一个计算子节点的函数,此处表明
//子节点的深度值永远大于父节点的深度值。
//但是暂不清楚子节点的来源。
@protected
void redepthChild(AbstractNode child) {
if (child._depth <= _depth) {
child._depth = _depth + 1;
child.redepthChildren();
}
}
//计算所有子节点的深度。
//该函数为空实现,想必是交由其子类具体实现,
//猜测:子类实现应该会利用redepthChild()进行深度计算。
void redepthChildren() { }
//owner对象的实际意义暂不清楚,应该是由子类确定
Object get owner => _owner;
Object _owner;
bool get attached => _owner != null;
//节点被添加到节点树中的时候被调用
//一般在父节点关联子节点的时候调用
@mustCallSuper
void attach(covariant Object owner) {
_owner = owner;
}
//节点从节点树中移除的时候会被调用
//一般在父节点移除子节点的时候调用
@mustCallSuper
void detach() {
_owner = null;
}
//声明了一个父节点属性
AbstractNode get parent => _parent;
AbstractNode _parent;
//在关联子节点时,该函数会触发子节点的attach()函数
//1.确定子节点的父引用
//2.将owner传递给子节点
//3.计算子节点的深度
@protected
@mustCallSuper
void adoptChild(covariant AbstractNode child) {
child._parent = this;
if (attached)
child.attach(_owner);
redepthChild(child);
}
//在移除子节点时,该函数会触发子节点的detach()函数
//1.删除子节点的父引用
//2.删除子节点的owner引用
@protected
@mustCallSuper
void dropChild(covariant AbstractNode child) {
child._parent = null;
if (attached)
child.detach();
}
}
从上面的代码中可以看出,AbstractNode对象可以通过属性"parent"形成一棵链式集合的节点树,并且可以按照深度进行排列,而且一棵节点树集合只管理同一个owner对象,也就是说同一棵节点树只会产生唯一的一个owner实例,并且每个节点对象对owner的引用都会在attach()被调用时指定,并且在detch()被调用时删除,由此可证明owner对象是整棵节点树的纽带所在。而RenderObject就具备了AbstractNode的所有特性。同时RenderObject也具备可点击的特性,HitTestTarget类代码结构如下所示:
abstract class HitTestTarget {
factory HitTestTarget._() => null;
void handleEvent(PointerEvent event, HitTestEntry entry);
}
抽象类HitTestTarget只存在一个可用函数handleEvent(),该函数需要传递两个不同类型的对象:PointerEvent和HitTestEntry,这里就简单的阐述一下这两个对象的作用,不详细进行代码分析了:
1. PointEvent为指针事件,其实就是点击事件的一个模型对象,里面存储了点击相关的数据,并且提供了跟矩阵转换Matrix4相关的函数,但是矩阵转换不影响事件本身。
2. HitTestEntry内部可以存储一个HitTestTarget和一个Matrix4对象。
总结,RenderObject具备了AbstractNode和HitTestTarget的特性,因此,任何实现了RenderObject的子类都可以组成一棵节点树并且都可以进行点击事件的传递。下面我们来具体分析一下其代码结构(这里我们单独提取其重要的函数来讲解):
class ParentData {
//当RenderObject从节点树中移除时调用
@protected
@mustCallSuper
void detach() { }
}
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
......
//重写了AbstractNode的owner()函数,使owner对象具备实际意义
PipelineOwner get owner => super.owner;
//每一个RenderObject对象都可以保存一个ParentData的对象
//该类型就名称而言意为父节点的数据,该类只存在一个detach()函数,
//而ParentData对象的detach函数是空实现,因此不同RenderObject的子类
//可能会实现不同ParentData的子类逻辑。
ParentData parentData;
//默认值为true,说明markNeedsLayout()函数执行无效
bool _needsLayout = true;
//根据markNeedsPaint()函数中的逻辑,我们知道:
//1.当_relayoutBoundary不等于本节点,代表需要重新布局父节点
//2.当_relayoutBoundary等于本节点&#x