基础 Widget
在 Fluter 中,几乎所有的都是一个 widget ,与原生开发不同的是,widget 的范围更加广阔,他不仅可以表示 UI 元素,也可以表示一些功能的组件,如手势检测的 widget,用于主题数据传递的 Theme
等等。所以,在大多数时候,可以认为 widget 就是一个控件,不必纠结于概念
Widget 的功能是 “描述一个 UI 元素的配置数据”,widget 并不是表示最终绘制在屏幕上的显示元素,正在代绘制屏幕上的是 Element
,下面就看一下 Element
Widget 与 Element
在 Flutter 中,Widget 的功能是 "描述一个 UI 元素的配置数据",也就是说,Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,它只是描述显示元素的一个配置数据
实际上,Flutter 中真正代表屏幕上显示元素的类是 Element
,也就是说 Widget 只是描述 Element
的配置数据,前期读者只需要知道:Widget 只是 UI 元素的一个配置数据,并且一个 Widget 可以对应多个 Element。这是因为同一个 Widget 可以被添加到 UI 树的不同部分,而真正渲染时,UI 树的每一个 Element
都会对应一个 Widget 对象 。总结一下:
- Widget 实际上就是
Element
的配置数据。Widget 树实际上是一个配置数,而真正渲染 UI 树是由Element
构成 不过由于是Element
是通过 Widget 生成的,所以他们之间是有对应关系,在大多数场景,我们可以广泛的认为 Widget 树就是指 UI 控件树或 UI 渲染树 - 一个 Widget 对象可以对应多个 Element。这个很好理解,根据同一份配置(Widget),可以创建多个实例(Element)
Widget 类
代码语言:javascript复制abstract class Widget extends DiagnosticableTree {
/// Initializes [key] for subclasses.
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 隐式调用的,在我们开发过程中基本不会调用到。
- _debugConcreteSubtype 复写父类的方法,主要是诊断树的一些特性
- canUpdate() 是一个静态方法,主要用于在 Widget 树重新更新 build 时服复用的 widget,其实具体来说,应该是:是否用新的 Widget 对象去更新旧 UI 树上所对应的
Element
对象的配置;通过其源码我们可以看到,只要newWidet
与oldWidget
的runtimeType
和key
同时相等时就会用newWidget
去更新Element
对象的配置,否则就会创建新的Element
。
另外 Widget
类本身是一个抽象类,其中最核心的就是定义了 createElement()
接口,在 Flutter 开发中,我们一般都不用直接继承 Widget
类来 实现一个新组建,想法,我们经常会通过继承 StatelessWidget 或 StatefulWidget 来间接继承 Widget
类,这两个类都继承自 Widget
类,并且这两个是非常重要的抽象类,它们引入了 Widget 中的两种模型。接下来 将重点介绍一下这两个类
StatelessWidget
- 无状态组件
- 继承自 Widget 类,重写了
createElement()
方法
@override
StatelessElement createElement() => StatelessElement(this);
创建 StatelessElement
对象,间接继承自 Element
类,与 StatelessWidget 相对应(作为其配置数据)
-
StatelessWidget
用于不需要维护状态的场景(也就是UI不可修改),它通常在build
方法中通过嵌套其他 Widget 来构建 UI ,在构建过程中会递归的构建其嵌套的 Widget。 - 栗子
class Echo extends StatelessWidget {
final String text;
final Color backgroundColor;
const Echo({Key key, @required this.text, this.backgroundColor: Colors.green})
: super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Text("hello word"),
color: backgroundColor,
)
);
}
}
上面代码,实现了一个回显字符串的 Echo
Widget
widget 的构造函数参数应使用命名参数,命名参数中的必要参数要添加
@required
标注,这样有利于静态代码分析器进行检查。另外,在继承widget
时,第一个参数通常key
,另外,如果 Widget 需要接收自 Widget,那么 child 或者 children 参数通常应该放在参数列表的最后。widget 的属性应该尽肯能被声明为 final,防止被意外改变
可以使用如下方式去使用它
代码语言:javascript复制void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Widget相关",
theme: ThemeData(primaryColor: Colors.blue),
home: Echo(text: "hello word"));
}
}
- Context
build
有一个context
参数,他是 BuildContext 类的实例,表示当前 widget 在 widget 树种的上下文,每个 widget 都会对应一个 context 对象(因为每个 widget都是 widget 树上的一个节点)。实际上,context 是当前 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;
}),
),
);
}
}
StatefulWidget
- 有状态的组件
- 和 StatelessWidget 一样,StatefulWidget 也是继承自 widget 类,并重写了 createElement 方法,不同的是返回的 Element 对象并不相同;另外 StatefulWidget 类中添加了一个新的接口 createState()
- 至少由两个类组成,一个 StatefulWidget ,一个 state 类
- StatefulWidget 类本身是不变的,但是 State 类中持有的状态在 widget 生命周期中可能会发生变化
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key? key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
@factory
State createState();
}
复制代码
-
StatefulElement
间接继承 Element 类,与 StatefulWidget 相对应(作为配置数据),S他特氟龙Element中可能会多次调用 createState 来创建状态(State)对象 -
createState
用于创建 Stateful widget 相关的状态,他在 Stateful widget 的生命周期中可能会被多次调用。例如,当一个 Stateful widget同时插入到 widget 树的多个未值日时,Flutter framework 就会调用该方法为每一个位置生成一个独立的 State 实例,其实,本质上就是一个 StatefulElement 对应一个 State 实例
Widget 树他可以指 widget 结构树,但是由于 widget 与 Element 有对应关系(一可能对多),在有些场景(Flutter 的 Sdk 文档中) 也代指 "UI树" 的意思
State
一个 StatefulWidget 会对应一个 State 类。State 表示与其对应的 StatefulWidget 要维护的状态,State 中保存的状态信息可以:
- 在 widget 构建时可以被同步读取
- 在 Widget 生命周期中可以被改变,当 State 被改变时,可以手动调用 setState() 方法通知 Flutter framework 状态发生改变,flutter framework 收到消息后,会调用其 build 方法重新构建 widget 树,从而达到更新 UI 的目的
State 中两个常用的属性
- widget :他表示与之关联的 widget 实例,由 Flutter framework 动态设置,不过这种关联并发永久,因为在生命周期中, UI 树上的某一节点 widget 实例自重新构建时可能会发生变化。但 State 实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,flutter framework 会动态设置 state,widget 为最新的 widget 实例
- context StatefulWidget 对应的 BuildContext,作用同 StatelessWidget 的 BuildContext 一致
State 生命周期
-
nitState
当 Widget 第一次插入到树中 Widget 时调用,对于每一个 State 对象,Flutter framework 只会调用一次该回调,所以通常在该回调中做一些一次性的操作,如状态初始化,订阅子树的时间通知等 不能再回调中调用 BuildContext.dependOnInheritedWidgetOfExactType ,原因是在初始化完成后,Widget 树中的InheritFromoWidget
也可以会发生变化,所以正确的做法应该是在 build 方法或者 didChangeDependencies 中调用它 -
didChangeDependencies()
当 State 对象依赖发生变化时会被调用 例如:build 中包含了一个 InheritedWidget,在之后的 build 中 InheritedWidget发生了变化,那么此时 InheritedWidget 的子 widget 的 didChangeDependencies 回调都会被调用。 典型的场景是当系统语言 Locale 或应用主题改变时, Flutter framework 会 调用 widget 进行回调 -
build()
主要是用来构建 Widget 子树的,会在如下场景被调用 1,在调用initState
之后 2,在调用didUpdateWidget()
之后 3,在调用setState()
之后 4,在调用didChangeDependencies()
之后 5,在 State 对象树中一个位置移除后(会调用 deactivate) 又重新插入到树的其他位置之后 -
reassemble()
此回调是专门为了开发调试而提供的,在热重载(hot reload) 时会被调用,此回调在 Release 模式下永远不会被调用 -
didUpdateWidget()
在 widget 重新构建时,Flutter framework 会调用Widget.canUpdate
来检测 Widget 树中同一个位置的新旧节点,然后去确定是否需要更新,如果 widget.canUpdate 返回 true 则会调用此回调。 正如之前所说, widget.canUpdate 会在新旧 widget 的 key 和 runtimeType 同时相等时返回 true,也就是说在新旧相等的 widget 的 额可以 和 runtimeType 同时相等时 此方法会被调用 -
deactivate()
当 State 对象从树中被移除时,会调用此回调。 在一些场景下,Flutter framework 会将 State 对象重新插入到树中,如果包含次 State 对象的子树在树的一个位置移动到另一个位置时(可以通过 GlobalKey 来实现)。如果移除之后没有重新插入到树中则紧接着就会调用dispose()
方法 -
dispose()
当 State 对象从树中被永久移除时调用;通常子此回调中释放资源
class CounterWidget extends StatefulWidget {
final int counter;
const CounterWidget({Key key, this.counter: 0}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _CounterWidget();
}
}
class _CounterWidget extends State<CounterWidget> {
int _counter;
@override
void initState() {
super.initState();
//初始化
_counter = widget.counter;
print("initState:初始化");
}
@override
void didUpdateWidget(covariant CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget:widget 重新构建');
}
@override
void deactivate() {
super.deactivate();
print('deactivate:State 被移除');
}
@override
void reassemble() {
super.reassemble();
print('reassemble:热重载');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies:State 对象依赖发生变化');
}
@override
void dispose() {
super.dispose();
print('dispose:State 永久移除');
}
@override
Widget build(BuildContext context) {
print('build:构建 widget');
return Scaffold(
body: Center(
child: FlatButton(
child: Text("$_counter"),
onPressed: () => setState(() => _counter),
)));
}
}
复制代码
一个计数器的小栗子,用来观察一下生命周期的变化
1,首先,打开这个页面,查看输出
代码语言:javascript复制I/flutter ( 6725): initState:初始化
I/flutter ( 6725): didChangeDependencies:State 对象依赖发生变化
I/flutter ( 6725): build:构建 widget
复制代码
2,点击热重载按钮,调用如下
代码语言:javascript复制I/flutter ( 6725): reassemble:热重载
I/flutter ( 6725): didUpdateWidget:widget 重新构建
I/flutter ( 6725): build:构建 widget
复制代码
3,点击数字按钮,调用如下
代码语言:javascript复制I/flutter ( 6725): build:构建 widget
复制代码
4,在 widget 树中移除 CountWidget
代码语言:javascript复制class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Widget相关",
theme: ThemeData(primaryColor: Colors.blue),
// home: CounterWidget(counter: 0)
home:Text("hello word")
);
}
}
复制代码
然后点击热重载,调用如下:
代码语言:javascript复制I/flutter ( 7366): deactivate:State 被移除
I/flutter ( 7366): dispose:State 永久移除
复制代码
生命周期图如下所示:
获取 State 对象
由于 StatefulWidget 的具体逻辑都在其 State 中,所有很多时候,我们都需要获取 StatefulWidget 对应的 State 对象来调用一些方法,对此,我们有两种方法在子 widget 树中获取父级 StatefulWidget 的 State 对象
通过 Context 获取
context 对象有一个 findAncestorStateOfType()
方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应 的 State 对象,
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
复制代码
通过 of 静态方法
代码语言:javascript复制 ScaffoldState _state = Scaffold.of(context);
复制代码
通过 GlobalKey
1,给目标 StatefulWidget 添加 GlobalKey
2,通过 GlobalKey 来获取 State 对象
代码语言:javascript复制//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
复制代码
注意:使用 GlobalKey 开销很大,如果有其他方案,应该去避免它,另外同一个 GlobalKey 在整个 widget 树中必须是惟一的,不能重复
本文参考自 Flutter实战(书籍)