2.3 组件复用与组合

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 绘制与动画

相关推荐
念格1 天前
Flutter 仿微信输入框最佳实践:自适应高度 + 超行数智能切换全屏
前端·flutter
程序员老刘1 天前
《Flutter跨平台开发核心技巧与应用》新书来了
flutter·ai编程·客户端
空中海1 天前
7.1 Flutter 性能模型
flutter
weixin_443478511 天前
Flutter学习之第三方组件:视频播放器控件
学习·flutter·音视频
空中海1 天前
11 Flutter 进阶与原理解析
flutter
于慨1 天前
项目flutter运行环境汇总
flutter
空中海1 天前
10 Flutter 测试与发布
flutter
空中海1 天前
12 Flutter 实战项目与最佳实践
flutter
里欧跑得慢2 天前
Flutter 测试全攻略:从单元测试到集成测试的完整实践
前端·css·flutter·web