最近在着手开发我的 《匠心星问》 ,它定位是一款 题库 应用,将集题目浏览、发布、解答、做题为一体。打算第一步先以 Flutter 为核心,准备题库资源。于是诞生《每日一题》 系列,准备精心设计一些 Flutter 的问题与解答,作为题库的养料。本文的焦点是探讨:
说说 Widget 的派生体系
Widget 是开发者搭建房子的材料,是 UI 界面搭建至关重要的因素。 Flutter 框架中有着丰富的组件可供开发者使用,对 Widget 派生体系的理解,能够站在更高的角度,审视整个构建系统,避免陷入 Widget 的泥沼中无法自拔。
- | - |
---|---|
1. Widget 的派生体系概览
Widget 作为构建用户界面的基本单元,其派生体系不仅奠定了框架的声明式 UI 基础,更通过巧妙的层次划分实现了高效渲染与灵活扩展。
Widget 派生体系最直观的表现形式,就是 类的继承体系
。(如下图所示)去除掉 _
相关的私有组件,就可以得到一幅派生体系的蓝图:
- RootWidget: 应用程序的根组件。
- PreferredSizeWidget: 向父组件提供首选尺寸。
- StatefulWidget: 可变状态组件。
- StatelessWidget: 不可变状态组件。
- ProxyWidget : 代理它的子组件,本身不负责 UI 构建,常用于数据共享和功能注入。
- RenderObjectWidget: 用于创建和配置底层渲染对象。
2.组合型: StatelessWidget 和 StatefulWidget
先从我们最最熟悉的 StatelessWidget 和 StatefulWidget 来说。Flutter 框架中估计有 500+ 的组件,其中 大约 80% 是 StatelessWidget 或 StatefulWidget。
之所以称它们是 组合型 Widget,是因为它们会通过 build
方法返回 Widget。而这些返回的 Widget 通常是由多个子组件 组合 而成的 UI 树结构。也就是说,它们本身并不直接控制布局或渲染,而是作为 UI 描述器
,通过组合其他 Widget 来构建特定的视图元件。
StatelessWidget —— 无状态组件
StatelessWidget
表示无状态的组件,其 UI 完全由构造函数传入的参数决定。它不能 主动
发生变化。如下所示中,只要 counter
不变, 每次构建出来的 UI 都是一样的。但外界可以通过创建不同参数的 CounterText
,来让界面展示不同的文字:
class CounterText extends StatelessWidget {
final int counter;
const HelloText({required this.name});
@override
Widget build(BuildContext context) {
return Text('Click $counter Times');
}
}
StatefulWidget —— 可变状态组件
相比之下,StatefulWidget
支持在生命周期中 维护状态并动态更新 UI 。常见的交互行为如点击计数、表单录入、动画表现等都依赖它来实现。它由两部分组成:
- 【1】 StatefulWidget 本身:负责持有配置数据,不负责组件构建;
- 【2】 State 类:由 StatefulWidget 创建,持有对应的组件对象。负责维护状态数据、
build
方法构建组件。
组件最重要的是构建视图元件,而 StatefulWidget
和 State
像是一种是 代理关系。StatefulWidget
将 UI 构建和状态管理责任的委托给 State
类处理,自身只是一个提供配置参数数据的 外壳
。
如下所示 ChangeableCounter
持有 Color 配置数据;而组件的构建逻辑交由 _ChangeableCounterState#build
来处理。State 中可以通过访问 widget
属性得到配置参数,从而可以对构建逻辑产生影响。比如这里通过组件中的 color 作为文字的颜色。
_ChangeableCounterState
不仅负责组件的构建,还要维护 _counter
状态数据,并在按钮点击时修改状态数据,并通过 setState
触发重新构建,来让视图发生变化。
class ChangeableCounter extends StatefulWidget {
final Color color;
const ChangeableCounter({super.key, required this.color});
@override
State createState() => _ChangeableCounterState();
}
class _ChangeableCounterState extends State<ChangeableCounter> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Click $counter Times' ,style: TextStyle(color: widget.color)),
ElevatedButton(
onPressed: () => setState(() => _counter++),
child: Text('Add'),
),
],
);
}
}
每当 StatefulWidget
对应的元素 (Element) 被创建之后,框架会调用其 createState()
方法生成一个 State
实例。之后,整个生命周期内,UI 的构建、状态的更新、生命周期的监听等,都是通过 State
这个“代理类”来完成的。
2. 劳苦功高的 RenderObjectWidget
StatelessWidget 和 StatefulWidget 是通过在 build 中组装 已有组件
来封装新的组件。打个比方,组合型 Widget 是将已有的人重新组合,形成解决特定问题的团队。团队的优势是 “调度”,而不是“干活”,那组成团队的人最终从哪里来呢? 真正的打工人就是 ---- RenderObjectWidget。
RenderObjectWidget
是 Flutter 渲染体系的桥梁。它不会直接参与 UI 构建,而是负责创建和配置底层的 RenderObject
,而 RenderObject
是真正完成底层绘制、布局、事件处理等。
比如我们耳熟能详的 Flex
、Wrap
、Padding
、Align
、Center
、DecoratedBox
等和布局、渲染直接相关的组件都是 RenderObjectWidget 体系下的,它们一般都有着特定的功能。
可以把 RenderObjectWidget
理解为一线工程师,它们直接操控画布、计算布局、参与命中测试。而 StatelessWidget
和 StatefulWidget
更像是项目经理,负责调用这些工程师,组织资源,整合接口,让界面层协同工作。
几个简单的例子,Container 是一个 StatelessWidget ,它内部的构建逻辑中整合了如下七个基本功能的 RenderObjectWidget
:
其价值在于可以将多个单子组件的树形组合,简化为一个组件。在语义方面更直观、简练(如下左图):
3. 数据传输公路 ProxyWidget
如果说 RenderObjectWidget
是 UI 的“施工者”,StatelessWidget
和 StatefulWidget
是“组织者”,那 ProxyWidget
则可以被称为 Flutter 中的数据传输公路 —— 它自己不参与 UI 构建、不负责渲染,却在 Widget 树中扮演着信息中继、功能注入的重要角色。那什么是 ProxyWidget,它究竟代理了个啥?
从派生体系中可以看出,ProxyWidget 派生类大名鼎鼎的 InheritedWidget
, 以及非常实用的 ParentDataWidget
。
- InheritedWidget 的特点是持有数据,可以向子树传递。子节点通过上下文访问可以建立订阅关系。
- ParentDataWidget 中会携带某类渲染对象感兴趣的数据,一般用于特定的组件。比如 Positioned 组件用于 Stack 之中,Flexible 用于 Flex 之中等。
- 它本身不打扰布局、不渲染画面,但一旦数据变了,它能精确告诉哪些组件需要更新。
这就像一条无形的数据高速公路,把状态、样式、主题等信息安全、高效地送达目的地。ProxyWidget 专注于功能注入和状态传递,不关心 UI 外观,只专注于控制行为和结构。
4. 存在感不高的 PreferredSizeWidget
相比于前面耳熟能详的组件,PreferredSizeWidget 似乎没什么人在意过。当有些组件想要对自身尺寸 “有个交代” 时,比如 AppBar
、TabBar
等 ,需要让父组件 Scaffold
知道它多高,以便正确布局整个页面。这就是 PreferredSizeWidget 的使用场景
PreferredSizeWidget
是一个抽象接口,它的职责是:
提供一个首选尺寸(preferredSize),供父组件在布局时参考。
注意,这只是一个建议尺寸,父组件可以采纳,也可以无视。它不会直接影响自身大小,而是为布局“提供建议”。这种“报备尺寸”的机制,在 Flutter 中使用场景虽然不多,但非常关键 —— 它是组合型组件与布局系统之间的一种契约协商方式。
5. 一切的起点 RootWidget
如果打开渲染组件树,可以看到最顶层的 root 对应的组件是 RootWidget ,它是由 Flutter 框架内部创建的树的起点。知不知道它,对应用层开发没什么影响。但理解 RootWidget 的诞生,对了解 Flutter 框架的原理非常重要,以后会作为单独的一题讨论。
6.本题小结
Flutter 的 Widget 派生体系,看似杂乱无章,但站在更高的角度审视时,可以看到精巧设计下的责任划分。不同类型的 Widget 各司其职,构建起高效、声明式的 UI 架构:
- StatelessWidget 与 StatefulWidget 是我们日常构建 UI 的主力军,它们提供了组合式开发的入口,关注“描述”与“响应”;
- RenderObjectWidget 负责创建和维护真实的渲染对象,提供功能组件的天命打工人;
- ProxyWidget 则打通了数据与行为的传输通道,确保信息在 Widget 树中流动顺畅,相当于
数据管道
; - PreferredSizeWidget 则为那些需要尺寸沟通的组件建立了契约机制,让 UI 能更智能地协同布局;
- 最终,一切都始于 RootWidget —— Flutter 应用启动的锚点,它是 Widget 树与底层引擎的结合点。
这就构成了 Widget 的派生体系 :将 UI 拆解为职责清晰、协作高效的部件,既提升了开发者的自由度,也为高性能的跨平台渲染奠定了基础。理解这些派生体系,不只是为了写出更多的 Widget,更是为了看清 Flutter 背后的设计思路 —— 每一层都是为了让 UI 更轻、更快、更可维护。
如果你有其他的看法,或者有什么想要的题目、或者想提供题目和答案,都欢迎在评论区留言。更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。