0
点赞
收藏
分享

微信扫一扫

Flutter学习 Widget简介


目录

  • ​​1. Widget 概述​​
  • ​​1.1 Widget概念​​
  • ​​1.2 Widget 分类​​
  • ​​2. Widget 接口​​
  • ​​3. StatelessWidget 和 StatefulWidget​​
  • ​​3.1 Flutter 中的四棵树​​
  • ​​3.2 StatelessWidget​​
  • ​​3.2.1 Context​​
  • ​​3.3 StatefulWidget​​
  • ​​3.4 State​​
  • ​​3.4.1 State 的生命周期​​
  • ​​3.4.2 build 方法为什么在 State 中而不是在 StatefulWidget 中​​
  • ​​3.4.3 在 Widget 树中获取 State 对象​​
  • ​​3.4.3.1 通过 Context获取​​
  • ​​3.4.3.2 通过 GlobalKey 获取​​
  • ​​3.5 通过 RenderObject 自定义 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组件的子类,这样做的好处是:

  1. 这样不会限制它的行为
    类比 Android,一个实现了复杂的效果的 Button视图 如果是继承的 FrameLayout,你会觉得它被限制了很多行为,它看起来是一个 Button,但它的逻辑却是一个 Layout
  2. 少写胶水代码
    例如上一点,本来想要给 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 框架的处理流程:

  1. 根据 Widget 树生成一个 Element 树, Element 树中的节点都继承自​​Element​​ 类
  2. 根据 Element 树生成 Render 树(即渲染树), 渲染树中的节点都继承自​​RenderObject​​ 类
  3. 根据 渲染树 生成 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树、渲染树如下图所示:

Flutter学习 Widget简介_ide


这里需要注意的是:

  • 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");
}

如下所示:

Flutter学习 Widget简介_flutter_02

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中的保存的状态信息可以:

  1. 在 Widget 构建时可以被同步读取
  2. 在 Widget 生命周期中可以改变,改变时, 可以手动调用其​​setState()​​​ 方法通知 Flutter 框架状态发生改变, Flutter 框架在接收到消息后,会重新调用​​StatefulWidget.build​​ 重新构建 Widget 树,已达到更新 UI 的目的

State 中两个常用属性:

  1. ​widget​​​,它表示与该 State 实例关联的 Widget实例 。 需要注意的是,这种关联不是永久的,因为 State 的实例只有在第一次插入树中会被创建, 而 StatefulWidget 因为改变,其实例会被多次创建, 那么​​State.widget​​ 就会被动态设置为新的 Widget
  2. ​context​​, 就是 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,新打开页面后,日志会输出:

Flutter学习 Widget简介_生命周期_03


在 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 的生命周期图如下所示:

Flutter学习 Widget简介_flutter_04

3.4.2 build 方法为什么在 State 中而不是在 StatefulWidget 中

前面介绍过, StatelessWidget 中是有 ​​build()​​​ 方法中,但与之对应的 StatefulWidget 却把 ​​build()​​ 方法放在了 State中,这是为什么呢?

这主要是为了提高开发的灵活性,如果将 ​​build()​​ 放在 StatefulWidget 主要有两个问题:

  1. 状态访问不便
    假如我们的 StatefulWidget 有很多的状态,而每次状态改变都要调用​​​build()​​​,由于状态是放在 State 中的,那么 build 和 State 放在两个类别中,构建时读取状态会很不方便。
    并且需要把 State 设置为公开状态,这会导致状态不再具有私密性,导致其修改会不可控。
  2. 继承​​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 来获取也是一个常用的方式,步骤为:

  1. 给目标​​StatefulWidget​​ 添加 GlobalKey:

//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)

  1. 通过 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 可以包含子组件,则可以根据子组件的数量来选择继承 ​​SingleChildRenderObject​​​ 或者 ​​MultiChildRenderObjectWidget​

  • ​createRenderObject()​​​ ,它是 RenderObjectWidget 中定义方法,该方法被组件对应的 Element 调用用于生成渲染对象。 我们的主要任务就是实现这个方法,返回渲染对象类的, 本例中是​​RenderCustomObject​
  • ​updateRenderObject()​​ 用于在组件树状态发生变化,单不需要重新创建 RenderObject 时用于更新组件渲染对象的回调

RenderCustomObject 类是继承 RenderBox, 而 RnederBox 继承自 ​​RenderObject​​,我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑,关于如何实现这些逻辑,以后会讲到。

参考

​​官方文档​​​​Flutter基础四​​


举报

相关推荐

0 条评论