2、Flutter组件化封装与复用实战:基础组件封装思想
大家好,欢迎来到第二章。
上一章我们聊了组件化能解决什么问题。这一章,咱们直接上手——聊聊基础组件的封装思想。
说白了,就是怎么把一个按钮、一个输入框、一个卡片,写成既好用又不容易出bug的样子。我做了几年Flutter,踩过不少坑,今天把这些经验掰开揉碎了讲给你听。
2.1 为什么要封装基础组件?
你想想看,一个项目里有多少个按钮?少说几十个,多则上百个。如果每个按钮都写一遍 ElevatedButton,改个圆角要全局搜索替换,那画面太美我不敢看。
封装的目的就三个:
- 统一风格:设计稿改了,改一个地方就行
- 减少重复:同样的代码不写第二遍
- 降低心智负担:团队成员不用记一堆参数,直接用封装好的组件
核心原则:封装不是把代码藏起来,而是把复杂度收起来,把灵活性露出来。
2.2 封装的第一层:参数化配置
最基础的封装,就是把组件的可变部分变成参数。我刚开始做的时候,就犯过这个错——把按钮写死了。
来看个反面教材:
// ❌ 别这么写
Widget buildButton(String text) {
return ElevatedButton(
onPressed: () {},
child: Text(text),
);
}
这个按钮啥都改不了。颜色、圆角、字体大小,全固定死了。换个场景就得重写一个。
正确的做法是这样的:
// ✅ 参数化配置
class AppButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final Color? backgroundColor;
final double? borderRadius;
final double? fontSize;
const AppButton({
Key? key,
required this.text,
this.onPressed,
this.backgroundColor,
this.borderRadius,
this.fontSize,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius ?? 8),
),
textStyle: TextStyle(fontSize: fontSize ?? 16),
),
child: Text(text),
);
}
}
嗯,这里要注意:参数要给默认值。调用方不传就用默认的,传了就覆盖。这样既灵活又省事。
2.3 封装的第二层:主题驱动
参数化虽然好,但有个问题——每个地方调用都要传一遍颜色、圆角,还是很啰嗦。
我在项目中遇到过这种情况:设计师说「所有主按钮蓝色,次按钮灰色」。如果每个按钮都传 backgroundColor: Colors.blue,改起来还是麻烦。
这时候就需要主题驱动了。Flutter 的 Theme 机制就是干这个的。
// ✅ 主题驱动封装
class AppButton extends StatelessWidget {
final String text;
final ButtonType type; // primary, secondary, danger
const AppButton({
Key? key,
required this.text,
this.type = ButtonType.primary,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Color bgColor;
switch (type) {
case ButtonType.primary:
bgColor = theme.colorScheme.primary;
break;
case ButtonType.secondary:
bgColor = theme.colorScheme.secondary;
break;
case ButtonType.danger:
bgColor = theme.colorScheme.error;
break;
}
return ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(backgroundColor: bgColor),
child: Text(text),
);
}
}
我的习惯:把组件需要的颜色、字体都定义在 ThemeData 里。组件只从主题取,不写死任何值。这样换肤、改设计都只需要改主题配置。
2.4 封装的第三层:组合与扩展
有时候,一个组件不够用。比如一个「带图标的按钮」,它本质上就是按钮 + 图标。
这时候不要重新写一个组件,而是用组合的方式:
// ✅ 组合扩展
class IconButton extends StatelessWidget {
final String text;
final IconData icon;
final VoidCallback? onPressed;
const IconButton({
Key? key,
required this.text,
required this.icon,
this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AppButton(
text: text,
onPressed: onPressed,
// 内部用 Row 组合图标和文字
// 这里只是示意,实际可以传一个 child builder
);
}
}
说白了,就是小组件搭积木。每个组件只做好一件事,然后组合成更复杂的组件。
2.5 避坑指南:封装过度
讲到这里,我得泼盆冷水。封装不是越多越好。
我曾经把一个 TextField 封装了七八层,结果改个光标颜色要穿透三层组件。后来我学乖了——
- 如果一个组件只有一两个地方用,别急着封装
- 如果一个组件的参数超过10个,说明拆得不够细
- 如果一个组件内部有超过3个 if-else,说明职责不单一
警告:封装过度比不封装更可怕。它会让代码变得难以理解和调试。记住:封装是为了让代码更好用,不是更复杂。
2.6 实战:封装一个通用卡片组件
光说不练假把式。咱们来封装一个实际项目中常用的卡片组件。
class AppCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final double? elevation;
final Color? backgroundColor;
final BorderRadiusGeometry? borderRadius;
const AppCard({
Key? key,
required this.child,
this.padding,
this.elevation,
this.backgroundColor,
this.borderRadius,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
elevation: elevation ?? 2,
color: backgroundColor ?? theme.cardColor,
shape: RoundedRectangleBorder(
borderRadius: borderRadius ?? BorderRadius.circular(12),
),
child: Padding(
padding: padding ?? const EdgeInsets.all(16),
child: child,
),
);
}
}
这个组件看起来简单,但我在项目中用得很爽。所有卡片风格统一,改圆角只需要改默认值。而且 child 是动态的,想放什么放什么。
2.7 总结一下
基础组件封装,说白了就是三件事:
- 参数化:把可变的东西变成参数,给默认值
- 主题化:从 Theme 取样式,不写死
- 组合化:小组件拼成大的,保持单一职责
嗯,这一章就到这。下一章咱们聊聊更复杂的——状态组件的封装。到时候会涉及到状态管理、生命周期这些硬核内容,做好准备。
记住:好的封装,是让调用方觉得「这东西就该这么用」。而不是「这玩意儿怎么这么多参数」。