赞
踩
在 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 调用之前不会重绘。
因为万物皆可 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组件的子类,这样做的好处是:
在 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
类是继承自 DiagnosticableTree
, DiagnosticableTree
即 “诊断树”,主要作用是提供调试信息Key
:类似于 React/Vue 中的 key
,主要的作用是决定是否在下一次 build 时复用旧的 Widget,决定的条件在 canUpdate方法中createElement()
Element
, Flutter 框架在构建 UI 树时,会先调用此方法生成对应节点的 Element
对象。此方法是 Flutter 框架隐式调用的, 在我们开发过程中基本不会调用到debugFillProperties()
canUpdate()
build
时复用旧的 Widget, 具体来说,应该是:是否用新的 Widget 对象去更新旧 UI 树上所对应的 Element 对象的配置, 通过源码我们可以看到,只要新旧 Widget 的 runtimeType 和 key相等时,就会用 newWidget 去更新 Element 对象的配置,否则就会创建新的 Element。Widget 本身是一个抽象类,其中最核心的就是定义了 createElement()
接口。在 Flutter 开发中,我们不会直接继承 Widget
类来实现组件,而是继承 StatelessWidget
或者 StatefulWidget
来间接继承 Widget 类。接下来来重点介绍这两个类。
来看看 Flutter 框架的处理流程:
Element
类RenderObject
类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树、渲染树如下图所示:
这里需要注意的是:
StatelessWidget
和 StatefulWidget
都没有对应的 RenderObjectStatelessWidget 继承自 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。
这里有几个注意的点:
requeired
关键字Key
child
或 children
参数通常应被放在参数列表的最后final
, 防止意外被改变然后我们可以在别的 Widget 里面通过如下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
如下所示:
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; }), ), ); } }
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 中, State 对象和 StatefulElement 具有一一对应的关系。所以在 Flutter 的 SDK 中,经常能看到注释:“从树中移除 State 对象” 或 “插入 State 对象”, 这里的树指的就是 Element 树。
State 表示的是预期对应的 StatefulWidget 要维护的状态, State中的保存的状态信息可以:
setState()
方法通知 Flutter 框架状态发生改变, Flutter 框架在接收到消息后,会重新调用 StatefulWidget.build
重新构建 Widget 树,已达到更新 UI 的目的State 中两个常用属性:
widget
,它表示与该 State 实例关联的 Widget实例 。 需要注意的是,这种关联不是永久的,因为 State 的实例只有在第一次插入树中会被创建, 而 StatefulWidget 因为改变,其实例会被多次创建, 那么 State.widget
就会被动态设置为新的 Widgetcontext
, 就是 BuildContextState 的生命周期对理解 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()
didChangeDependencies()
build()
中包含了一个 InheritedWidget
,然后在之后的 build()
中的 InheritedWidget
发生了变化,那么此时 Inherited Widget
的子 Widget 的 didChangeDependencies()
回调都会被调用。build()
initState()
后didUpdateWidget()
后setState()
后didChangeDependencies()
后reassemable()
didUpdateWidget()
Widget.canUpdate()
来检测 Widget 树中同一位置的新旧节点,然后决定是否需要更新,如果 Widget.canUpdate
返回 true,则会调用该回调。deactiveate()
dispose()
dispose()
StatefulWidget 的生命周期图如下所示:
前面介绍过, StatelessWidget 中是有 build()
方法中,但与之对应的 StatefulWidget 却把 build()
方法放在了 State中,这是为什么呢?
这主要是为了提高开发的灵活性,如果将 build()
放在 StatefulWidget 主要有两个问题:
build()
,由于状态是放在 State 中的,那么 build 和 State 放在两个类别中,构建时读取状态会很不方便。StatefulWidget
不便StatefulWidget 的逻辑都都是在其 State 中,所以很多时候,需要获取 StatefulWidget.State
对象来调用一些方法是,比如 Scaffold 组件打开 SnackBar 的逻辑就是放在其 State:ScaffoldState
中的。
我们有两种方法在 子 Widget 树中获取 父级 StatefulWidget 的State 对象。
有一个 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'),
);
}),
通过 GlobalKey 来获取也是一个常用的方式,步骤为:
StatefulWidget
添加 GlobalKey://定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
_globalKey.currentState.openDrawer()
GlobalKey 其实是 FLutter 提供的一种整个 App 中应用 element 的机制, 如果一个 Widget 设置了 GlobalKey, 我们可以通过
GlobalKey.currentWidget
获取该 Widget 对象GlobalKey.currentElement
获取该 Widget 对应的 Elment 对象GlobalKey.currentState
获取该 Widget 的 State 对象,前提是 StatefulWidgetStatelessWidget 和 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()
RenderCustomObject 类是继承 RenderBox, 而 RnederBox 继承自 RenderObject
,我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑,关于如何实现这些逻辑,以后会讲到。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。