在 Flutter 的世界里,流传着一句话:"Everything is a Widget"。但这不仅仅是一个营销口号,它是一种深刻的底层架构哲学。如果你只把 Widget 当作"UI 组件",那你只看到了冰山一角。这一章,我们将从"使用组件"的开发者,进化为"设计系统"的工程师。
3.1 组合优于继承:原子化的艺术
在传统的面向对象编程(如 Android 原生开发)中,如果你想创建一个带圆角的绿色按钮,你可能会去继承 Button 类。但在 Flutter 中,这种做法是被摒弃的。
核心哲学:组合 (Composition)
Flutter 鼓励通过组合小的、单一职责的 Widget 来构建复杂界面。
- 不要写:
class BigGreenButton extends Button { ... } - 要写:
GestureDetector+Container+Padding+Center+Text。
StatelessWidget vs StatefulWidget:抉择的艺术
- StatelessWidget (无状态): 它是"快照"。一旦创建,其内部属性不可变。适用于展示静态信息,如纯文本、图标。
- StatefulWidget (有状态): 它由两个类组成:Widget 类和 State 类。它是"长存的"。适用于数据会随交互、网络请求或时间而变化的场景。
避坑指南: 永远优先选择
StatelessWidget。只有当你确定需要调用setState()来重绘界面时,才升级为StatefulWidget。过度的状态管理会增加内存负担。
示例代码:组合出一个自定义卡片
dart
class ShadowCard extends StatelessWidget {
final Widget child;
const ShadowCard({required this.child, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 通过组合实现复杂效果,而不是继承某个 Card 类
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 8)],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: child,
),
);
}
}
3.2 布局约束机制:掌握"话语权"的博弈
这是所有 Flutter 新手最感头疼的地方。你是否遇到过:明明设置了 Container 宽高为 100,结果它却撑满了整个屏幕?
布局三部曲 (The Golden Rule)
- Constraints go down (约束向下传递): 父节点告诉子节点:"你最大能这么大,最小得这么大"。
- Sizes go up (尺寸向上传递): 子节点根据约束决定自己的实际大小,并回复给父节点。
- Parent sets position (父节点设置位置): 父节点根据子节点的大小,决定它在屏幕上的坐标。
深度剖析:为什么 Center 包裹 Container 会导致尺寸失效?
dart
Center(
child: Container(width: 100, height: 100, color: Colors.red),
)
- 逻辑:
Center接收到父级的"强约束"(如 375x812)。但Center是一个放松约束 的组件,它告诉Container:"你可以从 0 到 375 宽,0 到 812 高,随便选"。 - 结果:
Container此时拥有了自主权,它说:"那我要 100x100"。 - 对比: 如果去掉
Center,顶层约束会直接强制Container撑满屏幕,因为顶层约束通常是"紧约束"(必须等于屏幕大小)。
3.3 自定义渲染:下沉到 RenderObject 层
当 Container、Stack 和 CustomScrollView 无法满足你天马行空的想象力时,你需要动用"手术刀"------RenderObject。
RenderProxyBox:逻辑代理
如果你想修改现有组件的渲染行为(比如给所有的子组件加一个灰度滤镜),你可以使用 SingleChildRenderObjectWidget 配合 RenderProxyBox。
CustomPaint:像素级的自由
CustomPaint 是连接 Widget 世界与底层绘图引擎的桥梁。它提供了一个 Canvas,让你像在画布上一样挥毫泼墨。
示例代码:绘制一个自定义进度条
dart
class MyCirclePainter extends CustomPainter {
final double progress;
MyCirclePainter(this.progress);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 5
..style = PaintingStyle.stroke;
// 计算圆心和半径
Offset center = Offset(size.width / 2, size.height / 2);
double radius = size.width / 2;
// 绘制圆环
canvas.drawCircle(center, radius, paint..color = Colors.grey[200]!);
// 绘制进度弧线
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-1.57, // 从顶部开始
6.28 * progress, // 2 * PI * 进度
false,
paint..color = Colors.blue,
);
}
@override
bool shouldRepaint(MyCirclePainter oldDelegate) => oldDelegate.progress != progress;
}
3.4 进阶思考:渲染性能的边界
理解了 UI 构建系统后,你会明白:频繁的重绘是性能的死敌。
- RepaintBoundary: 如果你的页面中有一个复杂的背景动画,但上面的按钮是静态的。请给动画部分包裹一个
RepaintBoundary。这会为该子树创建一个独立的 Layer,避免按钮点击时导致整个背景重新 Paint。 - RelayoutBoundary: 同样,如果子组件的大小改变不会影响父组件(如固定宽高的子组件),Flutter 会自动将其设为重排边界,停止向上传递 Layout 信号。
总结:
Widget 不是终点,它只是我们与底层渲染引擎对话的协议。通过组合来设计结构,通过约束来掌控布局,通过自定义渲染来突破极限。这,就是 Flutter UI 构建系统的核心魅力。