Flutter学习 Widget简介
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习 Widget简介相关的知识,希望对你有一定的参考价值。
1. Widget 概述
1.1 Widget概念
在 Flutter 中,几乎所有的对象都是一个 Widget
,与原生的“控件”的,Flutter 中的 Widget 是一个更广泛的概念,正所谓一切皆可Widget, 它不仅可以表示 UI 元素,也可以表示一些功能性的组件,例如 Theme、GuestureDector等。
Flutter 的 Widget 其实就是 “组件”、“部件”、“控件”的概念, 因为其实际灵感是来源于 React, 所以其目标就是通过 Widget 嵌套 Widget 的方式来构建UI和进行逻辑处理。
和 android 的View相比,Widget 粗略的可以相当于View, Widget 和 View最大的不同是:Widget具有不同的生命周期,每当 Widget 或其状态状态发生变化时, Flutter 的框架都会创建一个新的 Widget实例树, 相比之下,Android 中的 View 会被绘制一次,并且在 invalidate 调用之前不会重绘。
1.2 Widget 分类
因为万物皆可 Widget, 所以 Widget 承载了基本所有的业务,自然而然也有各种各样的Widget,分类也有很多,主要包括下面这些类别:
Basics
: 基础组件,例如 Text、Button等Material Components
: 具有 Material Design 风格的组件Cupertino
:ios风格组件Accessibility
: 辅助功能的组件Animation
:动画组件Scrolling
:滚动组件Layout
:布局组件Async
:异步组件
Basics 比较特殊, 它并不是一个专门的类别组件,而是从其他官方Widget类中,选取一些常用的、易用的组件组成的类别,例如 Row 属于 Layout 组件的东西,但它也被选进了 Basics。
所以官方的意图是,在你开始构建第一个 Flutter 应用前,你可以通过学习 Basics 基础组件,来了解一些最常用的开发组件和知识。
Widget 更多的是以组合的形式存在,这其实体现良好的设计思想,因为在很多场景中,组合的设计结构是要比继承的结构好的。
例如 Container
是属于 Layout组件中的一个 Widget, 而 Container 又有 LimitedBox、ConstrainedBox、Aligin、Padding、DecoratedBox、 Transform 等部件来组成。如果想要实现 Container 的自定义效果,可以组合上面这些 Widget 以及其他简单的 Widget, 而不是把它写成某个Layout组件的子类,这样做的好处是:
- 这样不会限制它的行为
类比 Android,一个实现了复杂的效果的 Button视图 如果是继承的 FrameLayout,你会觉得它被限制了很多行为,它看起来是一个 Button,但它的逻辑却是一个 Layout - 少写胶水代码
例如上一点,本来想要给 Button 设置一个 Text,但是 FrameLayout 没有 setText方法,只能写这种胶水代码,来调用 Button 的 setText 方法
2. Widget 接口
在 Flutter 中, Widget 的功能是 “描述一个 UI 元素的配置信息”,也就是说 Widget 并不是表示最终绘制在设备屏幕上的显示元素,比如对 Text 来讲,文本的内容、文本样式等都是他的配置信息,来通过下面 Widget 代码,来看下一些 Widget使用到的接口:
@immutable // 不可变的
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;
}
...
}
@immutable
代表 Widget 是不可变的, 这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final),为什么不允许Widget中定义的属性变化呢? 这是因为 Widget 中的属性发生变化,Flutter会重新构建 Widget 树来替换旧的 Widget树,相当于自己的属性变了,自己就会被替换,这是无意义的。Widget
类是继承自DiagnosticableTree
,DiagnosticableTree
即 “诊断树”,主要作用是提供调试信息Key
:类似于 React/Vue 中的key
,主要的作用是决定是否在下一次 build 时复用旧的 Widget,决定的条件在 canUpdate方法中createElement()
一个 Widget 有多个Element
, Flutter 框架在构建 UI 树时,会先调用此方法生成对应节点的Element
对象。此方法是 Flutter 框架隐式调用的, 在我们开发过程中基本不会调用到debugFillProperties()
复写父类的方法,主要是设置诊断树的一些特性canUpdate()
是一个静态方法,他主要用于在 Widget 树重新build
时复用旧的 Widget, 具体来说,应该是:是否用新的 Widget 对象去更新旧 UI 树上所对应的 Element 对象的配置, 通过源码我们可以看到,只要新旧 Widget 的 runtimeType 和 key相等时,就会用 newWidget 去更新 Element 对象的配置,否则就会创建新的 Element。
Widget 本身是一个抽象类,其中最核心的就是定义了 createElement()
接口。在 Flutter 开发中,我们不会直接继承 Widget
类来实现组件,而是继承 StatelessWidget
或者 StatefulWidget
来间接继承 Widget 类。接下来来重点介绍这两个类。
3. StatelessWidget 和 StatefulWidget
3.1 Flutter 中的四棵树
来看看 Flutter 框架的处理流程:
- 根据 Widget 树生成一个 Element 树, Element 树中的节点都继承自
Element
类 - 根据 Element 树生成 Render 树(即渲染树), 渲染树中的节点都继承自
RenderObject
类 - 根据 渲染树 生成 Layer 树 ,然后上屏显示, Layer树中的节点都继承自
Layer
类
也就是说,真正的布局和渲染逻辑在 Render树中, Element 是 Widget 和 RenderObject 的中间态,用下面例子来说明,假设有一个 Widget 树:
Container( // 一个容器 widget
color: const Color.fromRGBO(0, 0, 100, 1), // 设置容器背景色
child: Row( // 可以将子widget沿水平方向排列
children: [
Image.network('https://www.example.com/1.png'), // 显示图片的 widget
const Text('A'),
],
),
);
如果 Container 设置了背景色, Container 内部会创建一个新的 ColoredBox
来填充背景,相关逻辑如下:
if (color != null)
current = ColoredBox(color: color!, child: current);
Image 内部会通过 RawImage
来渲染图片、 Text 内部会通过 RichText
来渲染文本,所以最终的 Widget树、 Element树、渲染树如下图所示:
这里需要注意的是:
- Widget 树 和 Element 树是一一对应的,但是和渲染树并不是。 比如
StatelessWidget
和StatefulWidget
都没有对应的 RenderObject - 渲染树在上屏前会生成 Layer 树,这个会在后面的原理讲到
3.2 StatelessWidget
StatelessWidget 继承自 Widget 类,重写了 createElement()
:
@override
StatelessElement createElement() => StatelessElement(this);
StatelessElement
间接继承自 Element
类, 与 StatelessWidget 是对应的。
StatelessWidget
的作用域是不需要维护状态的场景,它通常在 build
方法中通过嵌套其它 Widget 来构建UI,在构建过程中会递归的构建其嵌套的 Widget。 也就说它的一个主要场景是作为根布局容器。
来看下面一段官方代码:
class Echo extends StatelessWidget {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey, //默认为灰色
}):super(key:key);
final String text;
final Color backgroundColor;
@override
widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
上述代码实现了一个显示字符串的 Widget。
这里有几个注意的点:
- Widget 的构造函数必须要的传参要加入
requeired
关键字 - 在继承 Widget时,通常第一个参数是
Key
- 如果 Widget需要接受子Widget, 那么
child
或children
参数通常应被放在参数列表的最后 - Widget 的属性应尽可能的被声明为
final
, 防止意外被改变
然后我们可以在别的 Widget 里面通过如下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
如下所示:
3.2.1 Context
build() 中有一个 BuildContext
的传参,它是 BuildContext
类的一个实例,表示当前 Widget 在 Widget 树中的上下文,每一个 Widget 都有一个 Context对象。 实际上 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
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
3.3 StatefulWidget
StatefulWidget
也是继承了 Widget 类, 并重写了 createElement()
方法, 它返回的是一个 StatefulEment
对象。 另外 StatefulWidget
添加了一个新的接口 createState()
:
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key? key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
@factory
State createState();
StatefulElment
间接继承自Element
类, 与 StatefulWidget 对应。 StatefulElement 中可能会多次调用createElement
来创建状态对象createState
用于创建和 StatefulWidget 相关的状态,它在 StateWidget 的生命周期中可能会被多次调用。
例如, 当一个 StatefulWidget 同时插入到 Widget 树的多个位置时, Flutter 框架就会调用该方法为每一个位置生成独立的 State实例,本质上一个 StatefulElement 对应一个 State 实例
在 StatefulWidget 中, State 对象和 StatefulElement 具有一一对应的关系。所以在 Flutter 的 SDK 中,经常能看到注释:“从树中移除 State 对象” 或 “插入 State 对象”, 这里的树指的就是 Element 树。
3.4 State
State 表示的是预期对应的 StatefulWidget 要维护的状态, State中的保存的状态信息可以:
- 在 Widget 构建时可以被同步读取
- 在 Widget 生命周期中可以改变,改变时, 可以手动调用其
setState()
方法通知 Flutter 框架状态发生改变, Flutter 框架在接收到消息后,会重新调用StatefulWidget.build
重新构建 Widget 树,已达到更新 UI 的目的
State 中两个常用属性:
widget
,它表示与该 State 实例关联的 Widget实例 。 需要注意的是,这种关联不是永久的,因为 State 的实例只有在第一次插入树中会被创建, 而 StatefulWidget 因为改变,其实例会被多次创建, 那么State.widget
就会被动态设置为新的 Widgetcontext
, 就是 BuildContext
3.4.1 State 的生命周期
State 的生命周期对理解 Flutter 是非常重要的。下面来通过官方的例子来学习 State 的生命周期。
实现一个计数器的功能 CounterWidget 组件, 点击可以使得计数+1,由于要保存计数器的数值状态,所以我们应继承 StatefulWidget,代码如下:
class CounterWidget extends StatefulWidget {
const CounterWidget({Key? key, this.initValue = 0});
final int initValue;
@override
State<StatefulWidget> createState() => _CounterWidgetState();
}
CounterWidget
接受一个 initValue 的整型,它表示计数器的初始值,而 createState()
方法则创建一个 CounterWidgetState 的 State,用于绑定该 Widget ,来看下 State 的代码:
class CounterWidget extends StatefulWidget {
const CounterWidget({Key? key, this.initValue = 0});
final int initValue;
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
@override
void initState() {
super.initState();
_counter = widget.initValue;
print("init State :$_counter");
}
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: TextButton(
child: Text("$_counter"),
// 点击事件, 点击后自增
onPressed: () =>
setState(() {
++_counter;
}),
),
),
);
}
@override
void didUpdateWidget(covariant CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}
@override
void deactivate() {
super.deactivate();
print("deactivate");
}
@override
void dispose() {
super.dispose();
print("dispose");
}
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}
接下来使用初始页来打开一个新路由,在新路由里面只显示这个 Widget,新打开页面后,日志会输出:
在 StatefulWidget 插入到 Widget 树时, State 的 initState()
会被调用
然后点击 ⚡️ 按钮热重载,控制台会输出下面的日志:
reassemble
deactive
dispose
在 Counter 从 Widget 树中移除时, deactvie
和 dispose
会被依次调用,下面来看看各个回调函数:
initState()
当 Widget 第一次插入到 Widget 树时会被调用。 对于每一个State对象,Flutter只会调用一次该回调,通常都是在该回调中做一次性的操作,例如状态初始化、订阅子树的事件通知等。didChangeDependencies()
当 State 对象的依赖发生变化时会被调用,例如:在之前build()
中包含了一个InheritedWidget
,然后在之后的build()
中的InheritedWidget
发生了变化,那么此时Inherited Widget
的子 Widget 的didChangeDependencies()
回调都会被调用。
例如系统语言Locale、主题改变时,就会调用该回调通知。build()
主要用于构建 Widget 子树。会在如下场景被调用:
①:调用initState()
后
②:调用didUpdateWidget()
后
③:调用setState()
后
④:调用didChangeDependencies()
后
⑤:在 State对象从树中一个位置移除后又重新插入到树的其它位置之后reassemable()
专门为开发调试使用的, 仅在 热重载 时会被调用,在 Release 下永远不会被调用didUpdateWidget()
在 Widget 重新构建时, Flutter 框架会调用Widget.canUpdate()
来检测 Widget 树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate
返回 true,则会调用该回调。deactiveate()
当 State 对象从树中被移除时,会调用此回调,在一些场景下, Flutter 框架会将 State 对象重新插入到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时。 如果移除后没有重新插入到树中会紧接着调用dispose()
dispose()
当 State 对象从树中被永久移除时调用,一般在这个回调中释放资源。
StatefulWidget 的生命周期图如下所示:
3.4.2 build 方法为什么在 State 中而不是在 StatefulWidget 中
前面介绍过, StatelessWidget 中是有 build()
方法中,但与之对应的 StatefulWidget 却把 build()
方法放在了 State中,这是为什么呢?
这主要是为了提高开发的灵活性,如果将 build()
放在 StatefulWidget 主要有两个问题:
- 状态访问不便
假如我们的 StatefulWidget 有很多的状态,而每次状态改变都要调用build()
,由于状态是放在 State 中的,那么 build 和 State 放在两个类别中,构建时读取状态会很不方便。
并且需要把 State 设置为公开状态,这会导致状态不再具有私密性,导致其修改会不可控。 - 继承
StatefulWidget
不便
子类继承 StatefulWidget 类,意味着要做状态传递,做状态传递是毫无意义的,具体可以参考:为什么不将 build 方法放在StatefulWidget上
3.4.3 在 Widget 树中获取 State 对象
StatefulWidget 的逻辑都都是在其 State 中,所以很多时候,需要获取 StatefulWidget.State
对象来调用一些方法是,比如 Scaffold 组件打开 SnackBar 的逻辑就是放在其 State:ScaffoldState
中的。
我们有两种方法在 子 Widget 树中获取 父级 StatefulWidget 的State 对象。
3.4.3.1 通过 Context获取
有一个 context.findAncestorStateOfType()
方法,该方法可以从当前节点沿着 Widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象,下面是实现打开 SnackBar 的示例:
class GetStateObjectRoute extends StatefulWidget {
const GetStateObjectRoute({Key? key}) : super(key: key);
@override
State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("子树中获取State对象"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单1'),
);}),],),),
drawer: Drawer(),
);
}
}
一般来说, 如果 StatefulWidget 的状态是私有的,那么就不应该去直接获取其 State 的对象,因为其不希望被暴露出来。
相反的,如果 StatefulWidget 的状态是暴露出来的,我们就可以去获取。
但通过 context.findAncestorStateOfType()
获取 StetefulWidget 的状态的方法是通用的,我们并不能在语法层面上指定 StatefulWidget 的状态是否为私有。
所以在Flutter开发中有一个潜规则:如果 StatefulWidget 的状态是希望暴露出来的,应该在 StatefulWidget 中提供一个
of()
的静态方法来获取其 State 对象,开发者可以直接通过该方法来获取,如果不希望暴露,则不提供该方法
Scaffold 也提供了一个 of 方法,我们可以直接调用它:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
// 打开抽屉菜单
_state.openDrawer();
},
child: Text('打开抽屉菜单2'),
);
}),
3.4.3.2 通过 GlobalKey 获取
通过 GlobalKey 来获取也是一个常用的方式,步骤为:
- 给目标
StatefulWidget
添加 GlobalKey:
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
- 通过 GlobalKey 来获取 State 对象
_globalKey.currentState.openDrawer()
GlobalKey 其实是 FLutter 提供的一种整个 App 中应用 element 的机制, 如果一个 Widget 设置了 GlobalKey, 我们可以通过
GlobalKey.currentWidget
获取该 Widget 对象GlobalKey.currentElement
获取该 Widget 对应的 Elment 对象GlobalKey.currentState
获取该 Widget 的 State 对象,前提是 StatefulWidget
3.5 通过 RenderObject 自定义 Widget
StatelessWidget 和 StatefulWidget 都是用于组合组件的, 他们本身没有对应的 RenderObject。
Flutter 库中很多基础组件都不是通过 StatelessWidget 和 StatefulWidget 实现的, 例如 Text、Colume、Align。他们都是积木,“元组件”,而这些元组件都是通过自定义 RenderObject
来实现的。
实际上 Flutter 最原始定义组件的方式就是通过定义 RnederObject 来实现, 用官方示例来简单演示一下通过 RenderObject 定义组件的方式:
class CustomWidget extends LeafRenderObjectWidget{
@override
RenderObject createRenderObject(BuildContext context) {
// 创建 RenderObject
return RenderCustomObject();
}
@override
void updateRenderObject(BuildContext context, RenderCustomObject renderObject) {
// 更新 RenderObject
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox{
@override
void performLayout() {
// 实现布局逻辑
}
@override
void paint(PaintingContext context, Offset offset) {
// 实现绘制
}
}
如果组件不会包含子组件,则可以直接继承 LeafRenderObjectWidget, 它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承 子Widget,如下所示:
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
const LeafRenderObjectWidget({ Key? key }) : super(key: key);
@override
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}
它返回的 Element 是一个 LeafRenderObjectElement
,如果自定义的 Widget 可以包含子组件,则可以根据子组件的数量来
以上是关于Flutter学习 Widget简介的主要内容,如果未能解决你的问题,请参考以下文章