Flutter 通用列表项封装实战:适配多场景的 ListItemWidget

在 Flutter 开发中,列表项是界面的 "基石"------ 设置页、消息列表、商品列表、联系人页等场景都离不开它。原生ListTile虽然基础,但存在样式固化、扩展困难、重复编码导致样式不统一等问题。本文封装的ListItemWidget,通过灵活配置实现 "一套组件适配全场景",支持左侧图标 / 图片(圆形 / 方形)、右侧箭头 / 开关 / 角标 / 自定义组件,还能自定义样式和交互,直接复制即可集成到项目!

一、核心需求拆解(直击开发痛点)

封装前明确列表项的高频使用场景,确保组件通用性和灵活性:

  • ✅ 左侧布局:支持图标、网络图片(圆形 / 方形可配置),尺寸自定义,优先级可控制
  • ✅ 右侧布局:支持箭头(默认)、开关、文本、数字角标、自定义组件,覆盖绝大多数场景
  • ✅ 中间内容:支持主标题、副标题,样式可自定义(字体大小、颜色、粗细)
  • ✅ 样式自定义:背景色、内边距、组件间距、分割线(显示 / 隐藏 / 缩进)均可配置
  • ✅ 交互反馈:支持点击事件、长按事件,内置水波纹效果,符合原生交互习惯
  • ✅ 异常处理:图片加载失败有占位图,文字溢出自动截断,保证 UI 稳定性

二、完整代码实现(优化版,更灵活更易用)

dart

复制代码
import 'package:flutter/material.dart';

// 右侧组件类型枚举(覆盖高频场景,支持扩展)
enum RightWidgetType {
  arrow,      // 箭头(默认,跳转标识)
  switchBtn,  // 开关(适配开启/关闭场景)
  text,       // 文本(显示说明/状态)
  badge,      // 数字角标(消息提醒/未读计数)
  custom,     // 自定义组件(灵活适配特殊场景)
}

// 左侧图片形状枚举(新增,支持圆形/方形)
enum LeadingShape {
  circle,     // 圆形(默认,适配头像)
  square,     // 方形(适配图标/商品图片)
  rounded,    // 圆角方形(中间形态)
}

/// 通用列表项组件(适配90%+列表场景)
class ListItemWidget extends StatelessWidget {
  // 必选参数:主标题
  final String title;

  // 左侧组件配置
  final Widget? leadingIcon;          // 左侧图标(优先级高于图片)
  final String? leadingImage;         // 左侧网络图片地址
  final double leadingSize;           // 左侧组件尺寸(默认40px)
  final LeadingShape leadingShape;     // 左侧图片形状(默认圆形)
  final double leadingRadius;         // 左侧图片圆角(仅方形/圆角方形生效,默认8px)
  final Color leadingPlaceholderColor;// 图片加载占位色
  final double leadingSpacing;        // 左侧组件与标题间距(默认15px)

  // 右侧组件配置
  final RightWidgetType rightType;    // 右侧组件类型(默认箭头)
  final String? rightText;            // 右侧文本(text/badge类型用)
  final bool? switchValue;            // 开关状态(switchBtn类型用)
  final ValueChanged<bool>? onSwitchChanged; // 开关回调
  final Widget? customRightWidget;    // 自定义右侧组件(custom类型用)
  final Color badgeColor;             // 角标背景色(默认红色)
  final Color switchActiveColor;      // 开关激活色(默认蓝色)
  final Color switchInactiveColor;    // 开关未激活色(默认灰色)

  // 中间内容配置
  final String? subtitle;             // 副标题
  final TextStyle? titleStyle;        // 主标题样式
  final TextStyle? subtitleStyle;     // 副标题样式

  // 整体样式配置
  final bool showDivider;             // 是否显示分割线(默认true)
  final double dividerHeight;         // 分割线高度(默认1px)
  final double dividerIndent;         // 分割线左侧缩进(默认15px)
  final double dividerEndIndent;      // 分割线右侧缩进(默认0)
  final Color dividerColor;           // 分割线颜色(默认浅灰)
  final double padding;               // 列表项内边距(默认15px)
  final Color backgroundColor;        // 背景色(默认白色)
  final double borderRadius;          // 列表项圆角(默认0,支持卡片式)

  // 交互配置
  final VoidCallback? onTap;          // 点击事件
  final VoidCallback? onLongPress;    // 长按事件
  final bool isClickable;             // 是否可点击(默认true,控制水波纹显示)

  const ListItemWidget({
    super.key,
    required this.title,
    this.leadingIcon,
    this.leadingImage,
    this.leadingSize = 40.0,
    this.leadingShape = LeadingShape.circle,
    this.leadingRadius = 8.0,
    this.leadingPlaceholderColor = const Color(0xFFF5F5F5),
    this.leadingSpacing = 15.0,
    this.rightType = RightWidgetType.arrow,
    this.rightText,
    this.switchValue = false,
    this.onSwitchChanged,
    this.customRightWidget,
    this.badgeColor = Colors.red,
    this.switchActiveColor = Colors.blue,
    this.switchInactiveColor = Colors.grey,
    this.subtitle,
    this.titleStyle = const TextStyle(
      fontSize: 16.0,
      fontWeight: FontWeight.w500,
      color: Colors.black87,
    ),
    this.subtitleStyle = const TextStyle(
      fontSize: 14.0,
      color: Color(0xFF666666),
    ),
    this.showDivider = true,
    this.dividerHeight = 1.0,
    this.dividerIndent = 15.0,
    this.dividerEndIndent = 0.0,
    this.dividerColor = const Color(0xFFF5F5F5),
    this.padding = 15.0,
    this.backgroundColor = Colors.white,
    this.borderRadius = 0.0,
    this.onTap,
    this.onLongPress,
    this.isClickable = true,
  });

  /// 构建左侧组件(图标/图片)
  Widget? _buildLeadingWidget() {
    // 1. 优先显示图标(图标优先级 > 图片)
    if (leadingIcon != null) {
      return SizedBox(
        width: leadingSize,
        height: leadingSize,
        child: Center(child: leadingIcon), // 图标居中显示
      );
    }

    // 2. 显示网络图片
    if (leadingImage != null) {
      // 图片形状配置
      BorderRadiusGeometry borderRadius;
      switch (leadingShape) {
        case LeadingShape.circle:
          borderRadius = BorderRadius.circular(leadingSize / 2);
          break;
        case LeadingShape.square:
          borderRadius = BorderRadius.circular(0);
          break;
        case LeadingShape.rounded:
          borderRadius = BorderRadius.circular(leadingRadius);
          break;
      }

      return ClipRRect(
        borderRadius: borderRadius,
        child: Image.network(
          leadingImage!,
          width: leadingSize,
          height: leadingSize,
          fit: BoxFit.cover,
          // 加载占位图
          loadingBuilder: (context, child, loadingProgress) {
            if (loadingProgress == null) return child;
            return Container(
              width: leadingSize,
              height: leadingSize,
              color: leadingPlaceholderColor,
              child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
            );
          },
          // 错误占位图
          errorBuilder: (context, error, stackTrace) => Container(
            width: leadingSize,
            height: leadingSize,
            color: leadingPlaceholderColor,
            child: const Icon(Icons.error_outline, color: Colors.grey),
          ),
        ),
      );
    }

    // 无左侧组件
    return null;
  }

  /// 构建右侧组件(箭头/开关/文本/角标/自定义)
  Widget _buildRightWidget() {
    switch (rightType) {
      case RightWidgetType.arrow:
        return const Icon(
          Icons.arrow_forward_ios,
          size: 16.0,
          color: Color(0xFF999999),
        );

      case RightWidgetType.switchBtn:
        return Switch(
          value: switchValue!,
          onChanged: isClickable ? onSwitchChanged : null, // 不可点击时禁用开关
          activeColor: switchActiveColor,
          inactiveTrackColor: switchInactiveColor,
          materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // 缩小点击区域,适配布局
        );

      case RightWidgetType.text:
        return Text(
          rightText ?? '',
          style: subtitleStyle?.copyWith(color: const Color(0xFF999999)) ??
              const TextStyle(fontSize: 14.0, color: Color(0xFF999999)),
          overflow: TextOverflow.ellipsis, // 文本溢出截断
        );

      case RightWidgetType.badge:
        final badgeText = rightText ?? '0';
        return Container(
          padding: EdgeInsets.symmetric(horizontal: badgeText.length > 2 ? 4 : 6, vertical: 2),
          decoration: BoxDecoration(
            color: badgeColor,
            borderRadius: BorderRadius.circular(12.0), // 圆角角标,更美观
          ),
          child: Text(
            badgeText.length > 2 ? '99+' : badgeText, // 超过2位显示99+
            style: const TextStyle(color: Colors.white, fontSize: 12.0),
          ),
        );

      case RightWidgetType.custom:
        return customRightWidget ?? const SizedBox.shrink();
    }
  }

  @override
  Widget build(BuildContext context) {
    // 主体内容(左侧+中间+右侧)
    Widget content = Container(
      color: backgroundColor,
      padding: EdgeInsets.all(padding),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中对齐
        children: [
          // 1. 左侧组件
          if (_buildLeadingWidget() != null) ...[
            _buildLeadingWidget()!,
            SizedBox(width: leadingSpacing),
          ],
          // 2. 中间标题/副标题(占满剩余宽度)
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min, // 仅占用子组件高度,避免拉伸
              children: [
                // 主标题
                Text(
                  title,
                  style: titleStyle,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis, // 标题溢出截断
                ),
                // 副标题(可选)
                if (subtitle != null) ...[
                  const SizedBox(height: 4.0),
                  Text(
                    subtitle!,
                    style: subtitleStyle,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis, // 副标题溢出截断
                  ),
                ],
              ],
            ),
          ),
          // 3. 右侧组件(与中间内容间距)
          const SizedBox(width: 10.0),
          _buildRightWidget(),
        ],
      ),
    );

    // 添加工夫效果和圆角(如果配置了圆角)
    if (borderRadius > 0) {
      content = ClipRRect(
        borderRadius: BorderRadius.circular(borderRadius),
        child: content,
      );
    }

    // 交互包装(水波纹+点击/长按事件)
    Widget interactiveContent = InkWell(
      onTap: isClickable ? onTap : null,
      onLongPress: isClickable ? onLongPress : null,
      borderRadius: borderRadius > 0 ? BorderRadius.circular(borderRadius) : null,
      splashColor: Colors.grey.shade100, // 水波纹颜色
      highlightColor: Colors.transparent,
      child: content,
    );

    // 组合分割线
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        interactiveContent,
        // 分割线(可选)
        if (showDivider)
          Divider(
            height: dividerHeight,
            indent: dividerIndent,
            endIndent: dividerEndIndent,
            color: dividerColor,
          ),
      ],
    );
  }
}

三、实战使用示例(覆盖 80% 高频场景)

3.1 基础场景:设置页列表项(箭头跳转)

适配设置页 "功能入口 + 箭头" 的经典场景,如账号安全、隐私设置:

dart

复制代码
ListItemWidget(
  title: '账号与安全',
  leadingIcon: const Icon(Icons.security, color: Color(0xFF2196F3)),
  onTap: () {
    // 跳转账号安全页
    debugPrint('进入账号与安全设置');
  },
  // 自定义分割线缩进(与左侧图标对齐)
  dividerIndent: 40 + 15, // leadingSize + leadingSpacing
),

3.2 消息场景:带未读角标的列表项

适配消息列表、通知中心,显示未读数量:

dart

复制代码
ListItemWidget(
  title: '系统通知',
  leadingIcon: const Icon(Icons.notifications_active, color: Color(0xFFFF9800)),
  rightType: RightWidgetType.badge,
  rightText: '12', // 未读数量
  badgeColor: Color(0xFFFF5252), // 自定义角标颜色
  subtitle: '包含系统更新、活动通知等',
  onTap: () => debugPrint('查看系统通知'),
),

3.3 开关场景:功能开启 / 关闭(夜间模式)

适配需要切换状态的场景,如夜间模式、推送开关:

dart

复制代码
ListItemWidget(
  title: '夜间模式',
  leadingIcon: const Icon(Icons.dark_mode, color: Color(0xFF666666)),
  rightType: RightWidgetType.switchBtn,
  switchValue: true, // 当前开启状态
  switchActiveColor: Color(0xFF6200EE), // 自定义开关激活色
  onSwitchChanged: (value) {
    debugPrint('夜间模式切换为:$value');
    // 执行夜间模式切换逻辑
  },
  showDivider: false, // 隐藏分割线(列表最后一项)
),

3.4 商品场景:带图片的列表项

适配商品列表、联系人列表,左侧显示图片:

dart

复制代码
// 商品列表项
ListItemWidget(
  title: '2025新款夏季纯棉短袖T恤',
  leadingImage: 'https://example.com/tshirt.jpg', // 商品图片
  leadingShape: LeadingShape.rounded, // 圆角方形图片
  leadingRadius: 8.0,
  leadingSize: 60.0, // 放大左侧图片尺寸
  subtitle: '宽松百搭 | 多色可选',
  rightType: RightWidgetType.text,
  rightText: '¥99.00',
  titleStyle: const TextStyle(fontSize: 15.0, fontWeight: FontWeight.w400),
  subtitleStyle: const TextStyle(fontSize: 13.0, color: Color(0xFF888888)),
  backgroundColor: const Color(0xFFFAFAFA),
  padding: 12.0,
  onTap: () => debugPrint('查看商品详情'),
),

// 联系人列表项(圆形头像)
ListItemWidget(
  title: '张三',
  leadingImage: 'https://example.com/avatar.jpg',
  leadingShape: LeadingShape.circle, // 圆形头像
  leadingSize: 50.0,
  subtitle: '138****1234',
  rightType: RightWidgetType.custom,
  customRightWidget: const Icon(Icons.phone, size: 18, color: Color(0xFF2196F3)),
  onTap: () => debugPrint('拨打张三电话'),
),

3.5 自定义场景:右侧显示版本信息

适配特殊场景,右侧显示自定义组件(如版本号、状态标签):

dart

复制代码
ListItemWidget(
  title: '关于应用',
  leadingIcon: const Icon(Icons.info_outline, color: Color(0xFF4CAF50)),
  rightType: RightWidgetType.custom,
  customRightWidget: Row(
    children: [
      Text(
        'v3.13.0',
        style: TextStyle(color: Color(0xFF2196F3), fontSize: 14.0),
      ),
      const SizedBox(width: 4.0),
      Container(
        padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 1.0),
        decoration: BoxDecoration(
          color: Color(0xFFE3F2FD),
          borderRadius: BorderRadius.circular(4.0),
        ),
        child: const Text('最新版', fontSize: 11.0, color: Color(0xFF2196F3)),
      ),
    ],
  ),
  onTap: () => debugPrint('查看应用详情'),
),

3.6 卡片式场景:带圆角的列表项

适配卡片式布局,列表项带圆角和背景色:

dart

复制代码
ListItemWidget(
  title: '本月账单',
  leadingIcon: const Icon(Icons.account_balance_wallet, color: Color(0xFFFF5722)),
  subtitle: '已支出:¥2358.00',
  rightType: RightWidgetType.arrow,
  backgroundColor: const Color(0xFFF5F5F5),
  borderRadius: 12.0, // 列表项圆角
  padding: 16.0,
  showDivider: false, // 卡片式布局无需分割线
  onTap: () => debugPrint('查看详细账单'),
),

四、核心封装技巧(让组件更灵活、更优雅)

4.1 枚举分类:覆盖场景 + 便于扩展

通过RightWidgetTypeLeadingShape两个枚举,将右侧组件和左侧图片形状分类,既覆盖了高频场景,又便于后续扩展(如新增 "小红点" 右侧类型、"菱形" 图片形状)。

4.2 优先级设计:避免样式冲突

明确 "左侧图标优先级高于图片",当同时传入leadingIconleadingImage时,只会显示图标,避免样式混乱;同时通过isClickable控制交互状态,禁用时连开关也会同步禁用,逻辑更统一。

4.3 自适应布局:适配不同屏幕

  • 中间标题区域用Expanded包裹,确保右侧组件不会被挤压,文字溢出时自动截断(maxLines:1 + overflow: TextOverflow.ellipsis)。
  • 左侧组件尺寸、间距、分割线缩进均可配置,适配不同设计规范(如 iOS/Android 双端适配)。

4.4 交互体验优化:贴近原生习惯

  • 新增InkWell实现水波纹点击反馈,比单纯的GestureDetector更符合 Flutter 原生交互体验。
  • 开关组件添加materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,缩小点击区域,避免布局错位。
  • 角标超过 2 位数字时显示 "99+",避免角标过宽影响布局。

4.5 样式全自定义:告别 "硬编码"

原代码中很多样式(如角标颜色、开关颜色、分割线颜色)是硬编码的,优化后全部改为可配置参数,支持不同 APP 的设计风格(如浅色 / 深色模式、品牌色适配)。

4.6 异常处理:保证 UI 稳定性

  • 图片加载时显示加载指示器,加载失败显示错误图标,避免空白占位。
  • 文本溢出自动截断,避免文字超出屏幕导致布局错乱。
  • 开关回调、点击事件均做了非空判断,避免空指针异常。

五、避坑指南(实际开发必看)

  1. 分割线缩进适配 :当左侧有图标 / 图片时,分割线缩进建议设置为leadingSize + leadingSpacing,确保分割线与标题对齐,视觉更统一。
  2. 图片尺寸控制 :左侧图片尺寸不宜过大(建议 40-60px),避免挤压中间标题区域;同时设置fit: BoxFit.cover,保证图片比例正常。
  3. 圆角与裁剪 :当设置borderRadius时,需用ClipRRect包裹内容,否则背景色会溢出圆角;同时InkWellborderRadius要与列表项一致,水波纹才会贴合圆角。
  4. 开关状态同步 :使用switchBtn类型时,switchValue需与父组件状态绑定(如useState/Provider),否则开关状态会错乱。
  5. 长列表性能 :在ListView.builder中使用时,确保列表项是 "懒加载" 的,避免一次性创建过多组件;如果列表项背景色统一,可设置ListViewcacheExtent优化滚动性能。

六、进阶拓展场景(提升组件上限)

  1. 新增小红点功能 :扩展RightWidgetTypedot,实现右侧小红点提醒(无需数字),适配 "仅提醒未查看" 场景。
  2. 支持渐变背景 :新增gradientBackground参数,支持列表项渐变背景,适配更复杂的设计需求。
  3. 左侧多图标 :支持左侧显示多个图标(如 "推荐" 标签 + 主图标),通过List<Widget>? leadingIcons参数扩展。
  4. 副标题多行显示 :新增subtitleMaxLines参数,支持副标题多行显示,适配长文本场景。
  5. 动画效果 :添加onTap时的缩放动画,提升交互反馈质感。

总结

本文封装的ListItemWidget核心优势在于 "通用、灵活、统一":

  • 通用:覆盖设置页、消息列表、商品列表、联系人等 90%+ 场景,无需重复编码。
  • 灵活:样式、交互、布局均可自定义,适配不同设计规范和业务需求。
  • 统一:通过统一的组件封装,保证 APP 内所有列表项样式一致,提升产品质感。

https://openharmonycrossplatform.csdn.net/content

相关推荐
Howie Zphile1 小时前
做移动端的 Next.js 项目,可以选哪些 UI?
开发语言·javascript·ui
WX-bisheyuange1 小时前
基于Spring Boot的宠物商城网站设计与实现
前端·javascript·vue.js·毕业设计
9号达人1 小时前
大家天天说的'银弹'到底是个啥?看完这篇你就明白了
前端·后端·程序员
Non-existent9871 小时前
Flutter + FastAPI 30天速成计划自用并实践-第7天
flutter·oracle·fastapi
苏打水com1 小时前
第四篇:Day10-12 JS事件进阶+CSS动画——实现“复杂交互+视觉动效”(对标职场“用户体验优化”需求)
javascript·css·交互
初遇你时动了情1 小时前
react native实战项目 瀑布流、菜单吸顶、grid菜单、自定义背景图、tabbar底部菜单、轮播图
javascript·react native·react.js
BD_Marathon1 小时前
【JavaWeb】JavaScript使用var声明变量的特点
javascript
帅气马战的账号1 小时前
开源鸿蒙+Flutter:跨端开发的分布式协同与数据互通实践
flutter
longforus1 小时前
Flutter iOS 真机部署异常经验(Android Studio 提示无法运行,但 Xcode 可正常运行)
flutter·ios·android studio