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 总结一下

基础组件封装,说白了就是三件事:

  1. 参数化:把可变的东西变成参数,给默认值
  2. 主题化:从 Theme 取样式,不写死
  3. 组合化:小组件拼成大的,保持单一职责

嗯,这一章就到这。下一章咱们聊聊更复杂的——状态组件的封装。到时候会涉及到状态管理、生命周期这些硬核内容,做好准备。

记住:好的封装,是让调用方觉得「这东西就该这么用」。而不是「这玩意儿怎么这么多参数」。