Flutter 的设计哲学是"组合优于继承"。通过合理地封装与组合小 Widget,可以构建出可维护、可复用、易测试的 UI 系统。
一、组合与封装组件
1.1 基本封装原则
单一职责:每个组件只做一件事,避免臃肿的上帝组件。
dart
// ❌ 反例:将所有逻辑塞进一个 Widget
class ProductDetailPage extends StatefulWidget {
// 包含:商品图片、标题、价格、规格选择、评论、加购按钮...
// 代码量超过500行,难以维护
}
// ✅ 正例:拆分为多个小组件
class ProductDetailPage extends StatelessWidget {
const ProductDetailPage({super.key, required this.product});
final Product product;
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
ProductImageCarousel(images: product.images),
ProductInfoSection(product: product),
SpecificationSelector(specs: product.specs),
ReviewSection(productId: product.id),
],
),
bottomNavigationBar: AddToCartBar(productId: product.id),
);
}
}
1.2 提取可配置组件
dart
// 通用卡片组件
class AppCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final Color? backgroundColor;
final VoidCallback? onTap;
final double borderRadius;
final double elevation;
const AppCard({
super.key,
required this.child,
this.padding,
this.backgroundColor,
this.onTap,
this.borderRadius = 12,
this.elevation = 2,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: elevation,
color: backgroundColor ?? Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
child: InkWell(
borderRadius: BorderRadius.circular(borderRadius),
onTap: onTap,
child: Padding(
padding: padding ?? const EdgeInsets.all(16),
child: child,
),
),
);
}
}
// 复用示例
AppCard(
onTap: () => navigateToDetail(),
child: Row(
children: [
ClipRRect(borderRadius: BorderRadius.circular(8), child: productImage),
const SizedBox(width: 12),
Expanded(child: ProductInfo(product: product)),
],
),
)
二、Slot 插槽设计模式
插槽模式允许父 Widget 向子 Widget "注入" UI 内容,类似 Web 中的 <slot> 概念。
2.1 children 插槽
最基础的插槽模式:通过 children 参数传入子组件列表。
dart
class SectionCard extends StatelessWidget {
final String title;
final List<Widget> children; // ← 插槽
final Widget? trailing;
const SectionCard({
super.key,
required this.title,
required this.children,
this.trailing,
});
@override
Widget build(BuildContext context) {
return AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
if (trailing != null) trailing!,
],
),
const Divider(),
...children, // 展开插槽内容
],
),
);
}
}
// 使用:灵活传入不同内容
SectionCard(
title: '基本信息',
trailing: TextButton(onPressed: () {}, child: const Text('编辑')),
children: [
InfoRow(label: '姓名', value: user.name),
InfoRow(label: '手机', value: user.phone),
],
)
2.2 builder 插槽(Widget Builder)
builder 模式允许延迟构建,并可传入上下文信息:
dart
typedef ItemBuilder<T> = Widget Function(BuildContext context, T item, int index);
class AnimatedList<T> extends StatelessWidget {
final List<T> items;
final ItemBuilder<T> itemBuilder; // ← builder 插槽
final Widget? emptyPlaceholder;
const AnimatedList({
super.key,
required this.items,
required this.itemBuilder,
this.emptyPlaceholder,
});
@override
Widget build(BuildContext context) {
if (items.isEmpty) {
return emptyPlaceholder ?? const Center(child: Text('暂无数据'));
}
return ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) =>
itemBuilder(context, items[index], index), // 调用 builder 插槽
);
}
}
// 使用:传入自定义构建逻辑
AnimatedList<User>(
items: users,
emptyPlaceholder: const EmptyState(message: '还没有用户'),
itemBuilder: (context, user, index) => UserListTile(
user: user,
onTap: () => navigateTo(user),
),
)
2.3 多命名插槽
dart
class PageScaffold extends StatelessWidget {
final Widget? header; // 头部插槽
final Widget body; // 主体插槽(必填)
final Widget? footer; // 底部插槽
final Widget? floatingAction; // 浮动按钮插槽
const PageScaffold({
super.key,
this.header,
required this.body,
this.footer,
this.floatingAction,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: header != null
? PreferredSize(
preferredSize: const Size.fromHeight(120),
child: header!,
)
: null,
body: body,
floatingActionButton: floatingAction,
bottomNavigationBar: footer,
);
}
}
2.4 渲染拼接(Overlay / Portal 模式)
dart
class TooltipOverlay extends StatefulWidget {
final Widget child;
final WidgetBuilder tooltipBuilder; // builder 插槽:构建提示内容
const TooltipOverlay({
super.key,
required this.child,
required this.tooltipBuilder,
});
@override
State<TooltipOverlay> createState() => _TooltipOverlayState();
}
class _TooltipOverlayState extends State<TooltipOverlay> {
OverlayEntry? _overlay;
void _showTooltip() {
_overlay = OverlayEntry(
builder: (context) => Positioned(
top: 100,
left: 50,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(8),
child: widget.tooltipBuilder(context), // 使用 builder 插槽
),
),
);
Overlay.of(context).insert(_overlay!);
}
void _hideTooltip() {
_overlay?.remove();
_overlay = null;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onLongPress: _showTooltip,
onLongPressEnd: (_) => _hideTooltip(),
child: widget.child,
);
}
}
三、高级组合模式
3.1 Wrapper 模式(HOC)
dart
// 为任意 Widget 添加加载/错误状态
class AsyncWrapper<T> extends StatelessWidget {
final AsyncSnapshot<T> snapshot;
final Widget Function(T data) builder;
final Widget? loadingWidget;
final Widget Function(Object? error)? errorBuilder;
const AsyncWrapper({
super.key,
required this.snapshot,
required this.builder,
this.loadingWidget,
this.errorBuilder,
});
@override
Widget build(BuildContext context) {
return switch (snapshot.connectionState) {
ConnectionState.waiting => loadingWidget ?? const CircularProgressIndicator(),
ConnectionState.done when snapshot.hasError =>
errorBuilder?.call(snapshot.error) ?? Text('Error: ${snapshot.error}'),
ConnectionState.done when snapshot.hasData =>
builder(snapshot.data as T),
_ => const SizedBox.shrink(),
};
}
}
// 使用
FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) => AsyncWrapper(
snapshot: snapshot,
builder: (user) => UserProfile(user: user),
loadingWidget: const UserProfileSkeleton(),
errorBuilder: (e) => ErrorState(message: e.toString()),
),
)
3.2 Decorator 装饰器模式
dart
// Badge 装饰器:为任意 Widget 添加角标
class BadgeDecorator extends StatelessWidget {
final Widget child;
final int count;
final Color badgeColor;
const BadgeDecorator({
super.key,
required this.child,
required this.count,
this.badgeColor = Colors.red,
});
@override
Widget build(BuildContext context) {
if (count == 0) return child;
return Stack(
clipBehavior: Clip.none,
children: [
child,
Positioned(
top: -6,
right: -6,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: badgeColor,
shape: BoxShape.circle,
),
child: Text(
count > 99 ? '99+' : '$count',
style: const TextStyle(color: Colors.white, fontSize: 10),
),
),
),
],
);
}
}
// 使用
BadgeDecorator(
count: cartItemCount,
child: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () => navigateToCart(),
),
)
小结
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 组合封装 | 将复杂 UI 拆分为小组件 | 避免巨型 Widget |
| children 插槽 | 通过 List<Widget> 传入内容 |
列表型组件 |
| builder 插槽 | 通过回调函数延迟构建 | 需要上下文的动态内容 |
| 多命名插槽 | 多个具名 Widget 参数 | 布局框架组件 |
| Wrapper 模式 | 包装逻辑,内部调用 child | 加载/错误状态包裹 |
| Decorator 模式 | 在 child 外层追加 UI | 角标、边框、阴影 |
👉 下一节:2.4 绘制与动画