返回列表组合优于继承:Widget 设计模式
系统讲解Flutter组合式设计、Widget粒度拆分、Builder/Delegate模式、const优化及可复用组件设计

核心摘要:Flutter 的设计哲学与 GoF 的"优先使用对象组合而非类继承"高度一致——实际上,Flutter 官方文档明确指出:"Flutter uses composition over inheritance"。本文深入探讨 Flutter Widget 设计的核心模式:Builder 模式、HOC(高阶组件)、InheritedWidget 依赖注入、Slot 模式、Decorator 模式,并通过真实案例展示如何设计高度可复用、可维护的 Widget 库。
目录
- 为什么组合优于继承
- 基本组合:Widget 嵌套
- Builder 模式
- 高阶组件(HOC)模式
- InheritedWidget 依赖注入
- Slot 模式:灵活的 API 设计
- Decorator 模式
- 与 React / Java / iOS 的对比
- 实战案例:构建企业级组件库
- 深度追问
- 总结表格
1. 为什么组合优于继承
继承的问题
// ❌ 继承方式:脆弱、耦合、不灵活
class BaseButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
const BaseButton({super.key, required this.text, this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: onPressed, child: Text(text));
}
}
// 问题来了:想要添加 Icon 功能
class IconButton extends BaseButton { // ❌ 继承
final IconData icon;
const IconButton({super.key, required super.text, super.onPressed, required this.icon});
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(onPressed: onPressed, icon: Icon(icon), label: Text(text));
}
}
// 问题:想要 Loading + Icon + 圆角 的组合?
// 继承树会爆炸:LoadingIconRoundedButton extends ???
// 每种组合都需要一个新类 → 组合爆炸(Combinatorial Explosion)
Flutter组合优于继承—Widget设计模式与最佳实践|新宇宙博客组合的优势
// ✅ 组合方式:灵活、解耦、可复用
class AppButton extends StatelessWidget {
final String? text;
final Widget? icon;
final Widget? leading; // slot 模式
final Widget? trailing; // slot 模式
final bool isLoading;
final VoidCallback? onPressed;
final ButtonStyle? style;
const AppButton({
super.key,
this.text,
this.icon,
this.leading,
this.trailing,
this.isLoading = false,
this.onPressed,
this.style,
});
@override
Widget build(BuildContext context) {
Widget child = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (leading != null) ...[leading!, const SizedBox(width: 8)],
if (icon != null) ...[icon!, const SizedBox(width: 4)],
if (text != null) Text(text!),
if (trailing != null) ...[const SizedBox(width: 8), trailing!],
if (isLoading) ...[
const SizedBox(width: 8),
const SizedBox(
width: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
],
);
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: style,
child: child,
);
}
}
// 使用:通过参数组合各种变体
const AppButton(text: '提交');
const AppButton(text: '上传', icon: Icon(Icons.upload), isLoading: true);
AppButton(text: '删除', leading: Icon(Icons.delete, color: Colors.red));
2.1 装饰组合
// Flutter 自身就大量使用组合
// Container 实际上是多个基本 Widget 的组合
// Container 的源码等价物(简化)
Widget buildContainer({
Color? color,
EdgeInsets? padding,
EdgeInsets? margin,
BoxDecoration? decoration,
double? width,
double? height,
AlignmentGeometry? alignment,
required Widget child,
}) {
Widget result = child;
if (alignment != null) {
result = Align(alignment: alignment, child: result);
}
if (padding != null) {
result = Padding(padding: padding, child: result);
}
if (decoration != null || color != null) {
result = DecoratedBox(
decoration: decoration ?? BoxDecoration(color: color),
child: result,
);
}
if (width != null || height != null) {
result = ConstrainedBox(
constraints: BoxConstraints.tightFor(width: width, height: height),
child: result,
);
}
if (margin != null) {
result = Padding(padding: margin, child: result);
}
return result;
}
2.2 条件组合
// 避免 Widget 中出现 null,用 if-spread 模式
class FlexibleCard extends StatelessWidget {
final String title;
final String? subtitle;
final Widget? avatar;
final List<Widget>? actions;
const FlexibleCard({
super.key,
required this.title,
this.subtitle,
this.avatar,
this.actions,
});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
leading: avatar,
),
if (actions != null && actions!.isNotEmpty) ...[
const Divider(),
Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: actions!,
),
),
],
],
),
);
}
}
3. Builder 模式
// Builder 模式:将构建逻辑封装,提供局部 BuildContext
// 1. LayoutBuilder:响应约束变化
class ResponsiveGrid extends StatelessWidget {
final List<Widget> children;
const ResponsiveGrid({super.key, required this.children});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// 根据可用宽度决定列数
final columns = switch (constraints.maxWidth) {
< 600 => 1,
< 900 => 2,
< 1200 => 3,
_ => 4,
};
return GridView.count(
crossAxisCount: columns,
children: children,
);
},
);
}
}
// 2. Builder:创建局部 BuildContext(访问新的 InheritedWidget)
class ContextBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
// Builder 提供的 context 是 Scaffold 内部的 context
// 可以正确访问 Scaffold.of(context)
builder: (scaffoldContext) => ElevatedButton(
onPressed: () {
// ✅ 可以访问到 Scaffold(因为 scaffoldContext 在 Scaffold 内)
ScaffoldMessenger.of(scaffoldContext).showSnackBar(
const SnackBar(content: Text('Hello!')),
);
},
child: const Text('显示 Snackbar'),
),
),
);
}
}
// 3. 自定义 Builder Widget
typedef ItemBuilder<T> = Widget Function(BuildContext context, T item, int index);
class TypedListView<T> extends StatelessWidget {
final List<T> items;
final ItemBuilder<T> itemBuilder;
final Widget Function(BuildContext)? emptyBuilder;
final Widget Function(BuildContext)? loadingBuilder;
final bool isLoading;
const TypedListView({
super.key,
required this.items,
required this.itemBuilder,
this.emptyBuilder,
this.loadingBuilder,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
if (isLoading) {
return loadingBuilder?.call(context) ??
const Center(child: CircularProgressIndicator());
}
if (items.isEmpty) {
return emptyBuilder?.call(context) ??
const Center(child: Text('暂无数据'));
}
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => itemBuilder(context, items[index], index),
);
}
}
// 使用
TypedListView<User>(
items: users,
itemBuilder: (context, user, index) => UserCard(user: user),
emptyBuilder: (context) => const EmptyState(message: '还没有用户'),
isLoading: isLoading,
);
4. 高阶组件(HOC)模式
// HOC:接受 Widget/Builder,返回增强版 Widget
// 1. 加载状态包装器
class LoadingWrapper extends StatelessWidget {
final bool isLoading;
final String? loadingMessage;
final Widget child;
const LoadingWrapper({
super.key,
required this.isLoading,
this.loadingMessage,
required this.child,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
if (isLoading)
Container(
color: Colors.black54,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(color: Colors.white),
if (loadingMessage != null) ...[
const SizedBox(height: 16),
Text(loadingMessage!, style: const TextStyle(color: Colors.white)),
],
],
),
),
),
],
);
}
}
// 2. 错误边界(Error Boundary)
class ErrorBoundary extends StatefulWidget {
final Widget child;
final Widget Function(Object error, StackTrace? stack)? errorBuilder;
const ErrorBoundary({
super.key,
required this.child,
this.errorBuilder,
});
@override
State<ErrorBoundary> createState() => _ErrorBoundaryState();
}
class _ErrorBoundaryState extends State<ErrorBoundary> {
Object? _error;
StackTrace? _stackTrace;
@override
Widget build(BuildContext context) {
if (_error != null) {
return widget.errorBuilder?.call(_error!, _stackTrace) ??
ErrorWidget(_error!);
}
return widget.child;
}
}
// 注意:Flutter 的错误边界需要在 FlutterError.onError 中处理
// 上面的示例展示了组合模式的思路
// 3. 权限守卫 HOC
class PermissionGuard extends StatelessWidget {
final String permission;
final Widget child;
final Widget? fallback;
const PermissionGuard({
super.key,
required this.permission,
required this.child,
this.fallback,
});
@override
Widget build(BuildContext context) {
final hasPermission = PermissionService.of(context).has(permission);
if (!hasPermission) {
return fallback ?? const SizedBox.shrink();
}
return child;
}
}
// 4. 动画包装器 HOC
class AnimatedEntry extends StatefulWidget {
final Widget child;
final Duration duration;
final Curve curve;
const AnimatedEntry({
super.key,
required this.child,
this.duration = const Duration(milliseconds: 300),
this.curve = Curves.easeOut,
});
@override
State<AnimatedEntry> createState() => _AnimatedEntryState();
}
class _AnimatedEntryState extends State<AnimatedEntry>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(_animation),
child: widget.child,
),
);
}
}
// 使用:任何 Widget 都可以包装入场动画
AnimatedEntry(child: UserCard(user: user));
AnimatedEntry(
duration: const Duration(milliseconds: 500),
child: ProductGrid(products: products),
);
// InheritedWidget 是 Flutter 的依赖注入机制核心
// 定义主题/配置的 InheritedWidget
class AppThemeData {
final Color primaryColor;
final Color backgroundColor;
final TextStyle headlineStyle;
final double borderRadius;
const AppThemeData({
required this.primaryColor,
required this.backgroundColor,
required this.headlineStyle,
this.borderRadius = 8.0,
});
AppThemeData copyWith({
Color? primaryColor,
Color? backgroundColor,
TextStyle? headlineStyle,
double? borderRadius,
}) => AppThemeData(
primaryColor: primaryColor ?? this.primaryColor,
backgroundColor: backgroundColor ?? this.backgroundColor,
headlineStyle: headlineStyle ?? this.headlineStyle,
borderRadius: borderRadius ?? this.borderRadius,
);
}
class AppTheme extends InheritedWidget {
final AppThemeData data;
const AppTheme({
super.key,
required this.data,
required super.child,
});
// 静态访问方法
static AppThemeData of(BuildContext context) {
final theme = context.dependOnInheritedWidgetOfExactType<AppTheme>();
assert(theme != null, '没有找到 AppTheme,请确保在 AppTheme 的子树中使用');
return theme!.data;
}
// 可选访问(不强制依赖)
static AppThemeData? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>()?.data;
}
@override
bool updateShouldNotify(AppTheme oldWidget) {
// 仅数据真正改变时才通知依赖者重建
return data != oldWidget.data;
}
}
// 使用 InheritedWidget
class ThemedButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
const ThemedButton({super.key, required this.text, required this.onPressed});
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context); // 自动建立依赖
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(theme.borderRadius),
),
),
onPressed: onPressed,
child: Text(text, style: theme.headlineStyle),
);
}
}
// 在 App 根部提供
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppTheme(
data: AppThemeData(
primaryColor: Colors.blue,
backgroundColor: Colors.white,
headlineStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
child: MaterialApp(/* ... */),
);
}
}
6. Slot 模式:灵活的 API 设计
// Slot 模式:Widget 提供"插槽",调用者决定填什么
// 这是 Flutter 最重要的设计模式之一
class PageLayout extends StatelessWidget {
// 具名插槽
final Widget? header;
final Widget? sidebar;
final Widget body;
final Widget? footer;
final Widget? fab; // Floating Action Button 插槽
const PageLayout({
super.key,
this.header,
this.sidebar,
required this.body,
this.footer,
this.fab,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: header != null
? PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: header!,
)
: null,
drawer: sidebar,
body: body,
bottomNavigationBar: footer,
floatingActionButton: fab,
);
}
}
// 使用:自由组合
PageLayout(
header: const AppBar(title: Text('首页')),
sidebar: const NavigationDrawer(),
body: const HomeContent(),
footer: const BottomNav(),
fab: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
// 更进阶:Builder 插槽(延迟构建)
class AnimatedList2<T> extends StatefulWidget {
final List<T> items;
final Widget Function(BuildContext, T, int, Animation<double>) itemBuilder;
final Widget Function(BuildContext, int, Animation<double>)? separatorBuilder;
final Widget? emptyWidget;
const AnimatedList2({
super.key,
required this.items,
required this.itemBuilder,
this.separatorBuilder,
this.emptyWidget,
});
@override
State<AnimatedList2<T>> createState() => _AnimatedList2State<T>();
}
7. Decorator 模式
// Decorator:透明地为 Widget 添加功能,不改变其接口
// 1. 点击反馈 Decorator
class Tappable extends StatelessWidget {
final Widget child;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final BorderRadius? borderRadius;
const Tappable({
super.key,
required this.child,
this.onTap,
this.onLongPress,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: borderRadius,
child: child,
),
);
}
}
// 2. 间距 Decorator
extension WidgetSpacing on Widget {
Widget padAll(double value) => Padding(
padding: EdgeInsets.all(value),
child: this,
);
Widget padSymmetric({double h = 0, double v = 0}) => Padding(
padding: EdgeInsets.symmetric(horizontal: h, vertical: v),
child: this,
);
Widget margin(EdgeInsets insets) => Padding(
padding: insets,
child: this,
);
Widget center() => Center(child: this);
Widget expand([int flex = 1]) => Expanded(flex: flex, child: this);
Widget opacity(double value) => Opacity(opacity: value, child: this);
Widget onTap(VoidCallback callback) => GestureDetector(
onTap: callback,
child: this,
);
}
// 使用扩展方法的流畅 API
Text('Hello').padAll(16).center().opacity(0.8)
// 3. 骨架屏 Decorator
class Shimmer extends StatefulWidget {
final Widget child;
final bool isLoading;
const Shimmer({super.key, required this.child, this.isLoading = true});
@override
State<Shimmer> createState() => _ShimmerState();
}
class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
_animation = Tween(begin: -1.0, end: 2.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!widget.isLoading) return widget.child;
return AnimatedBuilder(
animation: _animation,
builder: (context, _) {
return ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: const [Color(0xFFE0E0E0), Color(0xFFF5F5F5), Color(0xFFE0E0E0)],
stops: [
_animation.value - 1,
_animation.value,
_animation.value + 1,
].map((s) => s.clamp(0.0, 1.0)).toList(),
).createShader(bounds),
child: widget.child,
);
},
);
}
}
// 任何 Widget 都可以骨架屏
Shimmer(
isLoading: isLoading,
child: UserCard(user: placeholderUser),
);
8. 与 React / Java / iOS 的对比
React 的组合模式
function Card({ header, children, footer }) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
<Card
header={<Title>标题</Title>}
footer={<Actions />}
>
<Content />
</Card>
function withLoading(WrappedComponent) {
return function LoadingWrapper({ isLoading, ...props }) {
if (isLoading) return <Spinner />;
return <WrappedComponent {...props} />;
};
}
Java / Android 的组合
interface Coffee {
double getCost();
String getDescription();
}
class SimpleCoffee implements Coffee { }
class MilkDecorator implements Coffee {
private Coffee coffee;
MilkDecorator(Coffee coffee) { this.coffee = coffee; }
@Override
public double getCost() { return coffee.getCost() + 0.5; }
@Override
public String getDescription() { return coffee.getDescription() + ", Milk"; }
}
iOS SwiftUI 对比
struct Highlighted: ViewModifier {
func body(content: Content) -> some View {
content
.background(Color.yellow)
.cornerRadius(5)
}
}
extension View {
func highlighted() -> some View {
modifier(Highlighted())
}
}
struct ConditionalView<Content: View>: View {
let condition: Bool
@ViewBuilder let content: () -> Content
var body: some View {
if condition { content() }
}
}
9. 实战案例:构建企业级组件库
// 企业级组件库设计:AppButton 完整实现
enum ButtonVariant { primary, secondary, destructive, ghost }
enum ButtonSize { sm, md, lg }
class AppButton extends StatelessWidget {
final String? label;
final Widget? icon;
final Widget? trailingIcon;
final ButtonVariant variant;
final ButtonSize size;
final bool isLoading;
final bool isDisabled;
final bool isFullWidth;
final VoidCallback? onPressed;
final String? tooltip;
const AppButton({
super.key,
this.label,
this.icon,
this.trailingIcon,
this.variant = ButtonVariant.primary,
this.size = ButtonSize.md,
this.isLoading = false,
this.isDisabled = false,
this.isFullWidth = false,
this.onPressed,
this.tooltip,
}) : assert(label != null || icon != null, '至少需要 label 或 icon');
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
final colors = _getColors(theme, variant);
final sizes = _getSizes(size);
Widget button = _buildButton(context, colors, sizes);
if (isFullWidth) {
button = SizedBox(width: double.infinity, child: button);
}
if (tooltip != null) {
button = Tooltip(message: tooltip!, child: button);
}
return button;
}
Widget _buildButton(BuildContext context, _ButtonColors colors, _ButtonSizes sizes) {
final isEffectivelyDisabled = isDisabled || isLoading;
return AnimatedOpacity(
opacity: isEffectivelyDisabled ? 0.6 : 1.0,
duration: const Duration(milliseconds: 200),
child: ElevatedButton(
onPressed: isEffectivelyDisabled ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: colors.background,
foregroundColor: colors.foreground,
side: colors.border != null
? BorderSide(color: colors.border!)
: BorderSide.none,
padding: sizes.padding,
minimumSize: Size(0, sizes.height),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(sizes.borderRadius),
),
elevation: variant == ButtonVariant.ghost ? 0 : null,
),
child: _buildChild(sizes),
),
);
}
Widget _buildChild(_ButtonSizes sizes) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && !isLoading) ...[
IconTheme(
data: IconThemeData(size: sizes.iconSize),
child: icon!,
),
if (label != null) SizedBox(width: sizes.gap),
],
if (isLoading) ...[
SizedBox(
width: sizes.iconSize,
height: sizes.iconSize,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
),
if (label != null) SizedBox(width: sizes.gap),
],
if (label != null)
Text(label!, style: TextStyle(fontSize: sizes.fontSize)),
if (trailingIcon != null) ...[
SizedBox(width: sizes.gap),
IconTheme(
data: IconThemeData(size: sizes.iconSize),
child: trailingIcon!,
),
],
],
);
}
_ButtonColors _getColors(AppThemeData theme, ButtonVariant variant) =>
switch (variant) {
ButtonVariant.primary => _ButtonColors(
background: theme.primaryColor,
foreground: Colors.white,
),
ButtonVariant.secondary => _ButtonColors(
background: Colors.grey.shade100,
foreground: Colors.grey.shade800,
border: Colors.grey.shade300,
),
ButtonVariant.destructive => _ButtonColors(
background: Colors.red.shade600,
foreground: Colors.white,
),
ButtonVariant.ghost => _ButtonColors(
background: Colors.transparent,
foreground: theme.primaryColor,
),
};
_ButtonSizes _getSizes(ButtonSize size) => switch (size) {
ButtonSize.sm => _ButtonSizes(
height: 32, padding: const EdgeInsets.symmetric(horizontal: 12),
fontSize: 13, iconSize: 14, gap: 4, borderRadius: 6,
),
ButtonSize.md => _ButtonSizes(
height: 40, padding: const EdgeInsets.symmetric(horizontal: 16),
fontSize: 14, iconSize: 16, gap: 6, borderRadius: 8,
),
ButtonSize.lg => _ButtonSizes(
height: 48, padding: const EdgeInsets.symmetric(horizontal: 20),
fontSize: 16, iconSize: 20, gap: 8, borderRadius: 10,
),
};
}
class _ButtonColors {
final Color background;
final Color foreground;
final Color? border;
_ButtonColors({required this.background, required this.foreground, this.border});
}
class _ButtonSizes {
final double height;
final EdgeInsets padding;
final double fontSize;
final double iconSize;
final double gap;
final double borderRadius;
_ButtonSizes({
required this.height, required this.padding,
required this.fontSize, required this.iconSize,
required this.gap, required this.borderRadius,
});
}
// 使用示例
class ButtonShowcase extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
AppButton(label: '提交', onPressed: () {}),
AppButton(label: '取消', variant: ButtonVariant.secondary, onPressed: () {}),
AppButton(label: '删除', variant: ButtonVariant.destructive, isLoading: true),
AppButton(icon: const Icon(Icons.add), label: '添加',
trailingIcon: const Icon(Icons.chevron_right)),
const AppButton(label: '禁用', isDisabled: true),
AppButton(label: '全宽按钮', isFullWidth: true, onPressed: () {}),
],
);
}
}
10. 深度追问
Q1:Flutter 官方为什么不推荐深度继承 Widget?
Flutter Widget 的继承有天然限制:build() 方法返回的是 Widget 树,而不是"绘制指令",所以"继承后修改部分绘制"的模式行不通。更重要的是,继承 Widget 会引入紧耦合——子类必须了解父类的内部实现,一旦父类修改(如 Flutter SDK 升级),子类可能失效。而组合方式只依赖公开接口,更稳定。Flutter 官方文档明确说:"prefer composition over inheritance for widget design"。
Q2:Builder 模式中的 context 为什么有时候必须用 Builder 包一层?
BuildContext 只能访问它在树中位置以上的 InheritedWidget。如果你在 Scaffold 的 build() 方法直接使用 Scaffold.of(context),这个 context 是 Scaffold 的父节点的 context,Scaffold 本身还没挂载,所以找不到。Builder 创建了一个新的 BuildContext,这个 context 在 Scaffold 内部,因此可以正确向上找到 Scaffold。
Q3:扩展方法(Extension Methods)实现的 Decorator 和 Widget 包装有什么取舍?
扩展方法更简洁(.padAll(16) vs Padding(padding: ..., child: ...)),但有局限:1)无法在扩展方法中维护状态(适合无状态装饰);2)链式调用顺序就是嵌套顺序,可能与视觉直觉相反(先写内层还是外层);3)IDE 代码提示可能不如具名构造函数清晰。实践中两者混合使用:简单布局调整用扩展方法,复杂功能封装用 Widget 组合。
11. 总结表格
| 模式 | 适用场景 | Flutter 示例 |
|---|
| 基本组合 | 复杂 UI 分解为简单 Widget | Container = Padding + DecoratedBox |
| Builder 模式 | 延迟构建、响应约束 | LayoutBuilder、FutureBuilder、StreamBuilder |
| HOC 模式 | 横切关注点(Loading、Auth、动画) | LoadingWrapper、AnimatedEntry |
| InheritedWidget | 全局数据/主题注入 | Theme、MediaQuery、Provider |
| Slot 模式 | 灵活可配置的 Widget API | Scaffold.appBar/drawer/body/fab |
| Decorator 模式 | 透明功能增强 | Padding、Opacity、Transform |
| 扩展方法 | 流式 API、简化写法 | .padAll()、.center()、.onTap() |
组合设计原则
1. 单一职责:每个 Widget 只做一件事
2. 开放封闭:通过参数/Slot 扩展,不修改内部实现
3. 依赖倒置:通过 InheritedWidget/callback 传递依赖,不向下硬编码
4. 最小 API:暴露最少的参数,通过默认值覆盖 80% 场景
5. 渐进式复杂度:简单用法简单,高级用法通过 Builder/Slot 支持