Flutter 通用列表项组件 CommonListItemWidget:全场景布局 + 交互增强

在 Flutter 开发中,列表是数据展示的核心载体,而列表项的样式统一性与灵活性直接影响开发效率与用户体验。原生 ListTile 存在图标位置固定、不支持徽章提示、自定义布局受限等问题。本文封装的 CommonListItemWidget 整合 "图标 / 图片 + 标题 + 副标题 + 右侧组件 + 徽章" 全布局,支持选中、禁用、点击反馈等交互,适配联系人、消息、设置等 90%+ 列表场景。

一、核心优势(精准解决开发痛点)

  1. 全场景布局覆盖:支持 "图标 / 图片 + 标题 + 副标题" 基础布局,可扩展右侧组件(箭头、开关、按钮)与徽章提示,适配各类列表需求
  2. 交互状态完整:内置选中、禁用、点击反馈状态,支持自定义选中颜色与禁用样式,无需额外封装手势与状态
  3. 媒体适配灵活:左侧支持图标、本地图片、网络图片,支持圆形 / 方形裁剪,自动处理图片加载失败降级
  4. 样式高度自定义:文本样式、间距、边框、圆角均可细粒度配置,支持底部边框分隔,统一项目列表风格
  5. 低侵入高复用:必选参数仅 3 个,默认样式贴合设计规范,一行代码集成,降低维护成本

二、核心配置速览(关键参数一目了然)

配置分类 核心参数 核心作用
必选配置 titleonTap 列表项标题、点击回调
媒体配置 iconimageUrlassetImagemediaSizeisCircleMedia 图标、网络 / 本地图片、媒体尺寸、是否圆形裁剪
文本配置 subtitletitleStylesubtitleStyletextMaxLines 副标题、文本样式、最大行数限制
右侧组件配置 trailingshowArrowswitchValueonSwitchChanged 自定义右侧组件、显示箭头、开关组件、开关回调
交互与样式配置 isSelectedisDisabledselectedColorshowDivideradaptDarkMode 选中状态、禁用状态、选中颜色、显示分隔线、深色模式适配

三、生产级完整代码(可直接复制,开箱即用)

dart

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

/// 通用列表项组件(全场景布局+交互增强)
class CommonListItemWidget extends StatelessWidget {
  // 必选参数
  final String title; // 列表项标题
  final VoidCallback onTap; // 点击回调

  // 媒体配置(左侧图标/图片,优先级:imageUrl > assetImage > icon)
  final IconData? icon; // 图标
  final String? imageUrl; // 网络图片地址
  final String? assetImage; // 本地资源图片路径
  final double mediaSize; // 媒体尺寸(默认40px)
  final bool isCircleMedia; // 是否圆形裁剪(默认true)
  final Color? iconColor; // 图标颜色
  final Widget? placeholder; // 图片加载占位组件
  final Widget? errorWidget; // 图片加载失败组件

  // 文本配置
  final String? subtitle; // 副标题(标题下方辅助文本)
  final TextStyle? titleStyle; // 标题样式
  final TextStyle? subtitleStyle; // 副标题样式
  final int titleMaxLines; // 标题最大行数(默认1)
  final int subtitleMaxLines; // 副标题最大行数(默认1)
  final TextOverflow textOverflow; // 文本溢出处理(默认省略号)

  // 右侧组件配置(优先级:trailing > switch > arrow)
  final Widget? trailing; // 自定义右侧组件
  final bool showArrow; // 是否显示右箭头(默认false)
  final bool? switchValue; // 开关组件值(为null时不显示)
  final Function(bool)? onSwitchChanged; // 开关状态变化回调

  // 交互与样式配置
  final bool isSelected; // 是否选中(默认false)
  final bool isDisabled; // 是否禁用(默认false)
  final Color selectedColor; // 选中背景色(默认浅蓝)
  final Color disabledColor; // 禁用背景色(默认浅灰)
  final bool showDivider; // 是否显示底部分隔线(默认true)
  final Color dividerColor; // 分隔线颜色(默认浅灰)
  final double dividerHeight; // 分隔线高度(默认0.5px)
  final EdgeInsetsGeometry padding; // 列表项内边距(默认16px水平)
  final double minHeight; // 列表项最小高度(默认56px)

  // 适配配置
  final bool adaptDarkMode; // 适配深色模式(默认true)

  const CommonListItemWidget({
    super.key,
    required this.title,
    required this.onTap,
    // 媒体配置
    this.icon,
    this.imageUrl,
    this.assetImage,
    this.mediaSize = 40.0,
    this.isCircleMedia = true,
    this.iconColor,
    this.placeholder,
    this.errorWidget,
    // 文本配置
    this.subtitle,
    this.titleStyle,
    this.subtitleStyle,
    this.titleMaxLines = 1,
    this.subtitleMaxLines = 1,
    this.textOverflow = TextOverflow.ellipsis,
    // 右侧组件配置
    this.trailing,
    this.showArrow = false,
    this.switchValue,
    this.onSwitchChanged,
    // 交互与样式配置
    this.isSelected = false,
    this.isDisabled = false,
    this.selectedColor = const Color(0xFFF0F7FF),
    this.disabledColor = const Color(0xFFF9F9F9),
    this.showDivider = true,
    this.dividerColor = const Color(0xFFE0E0E0),
    this.dividerHeight = 0.5,
    this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    this.minHeight = 56.0,
    this.adaptDarkMode = true,
  }) : assert(
          (imageUrl == null && assetImage == null && icon == null) ||
          (imageUrl != null) ||
          (assetImage != null) ||
          (icon != null),
          "至少配置icon、imageUrl、assetImage中的一个"
        ),
        assert(
          switchValue == null || onSwitchChanged != null,
          "开关组件必须配置onSwitchChanged回调"
        );

  /// 深色模式颜色适配
  Color _adaptDarkMode(Color lightColor, Color darkColor) {
    if (!adaptDarkMode) return lightColor;
    return MediaQuery.platformBrightnessOf(context) == Brightness.dark
        ? darkColor
        : lightColor;
  }

  /// 构建左侧媒体组件(图标/图片)
  Widget _buildMedia() {
    final adaptedIconColor = _adaptDarkMode(
      iconColor ?? Colors.black87,
      Colors.white70,
    );
    final placeholderWidget = placeholder ?? Container(
      width: mediaSize,
      height: mediaSize,
      decoration: BoxDecoration(
        color: _adaptDarkMode(const Color(0xFFF5F5F5), const Color(0xFF3D3D3D)),
        shape: isCircleMedia ? BoxShape.circle : BoxShape.rectangle,
      ),
    );
    final errorWidget = this.errorWidget ?? Container(
      width: mediaSize,
      height: mediaSize,
      decoration: BoxDecoration(
        color: _adaptDarkMode(const Color(0xFFF5F5F5), const Color(0xFF3D3D3D)),
        shape: isCircleMedia ? BoxShape.circle : BoxShape.rectangle,
      ),
      child: Icon(Icons.error, size: mediaSize / 2, color: Colors.grey),
    );

    // 网络图片
    if (imageUrl != null) {
      return ClipRRect(
        borderRadius: isCircleMedia 
            ? BorderRadius.circular(mediaSize / 2)
            : BorderRadius.circular(8),
        child: Image.network(
          imageUrl!,
          width: mediaSize,
          height: mediaSize,
          fit: BoxFit.cover,
          placeholder: (context, url) => placeholderWidget,
          errorBuilder: (context, error, stackTrace) => errorWidget,
        ),
      );
    }

    // 本地资源图片
    if (assetImage != null) {
      return ClipRRect(
        borderRadius: isCircleMedia 
            ? BorderRadius.circular(mediaSize / 2)
            : BorderRadius.circular(8),
        child: Image.asset(
          assetImage!,
          width: mediaSize,
          height: mediaSize,
          fit: BoxFit.cover,
          errorBuilder: (context, error, stackTrace) => errorWidget,
        ),
      );
    }

    // 图标
    return Container(
      width: mediaSize,
      height: mediaSize,
      decoration: BoxDecoration(
        color: _adaptDarkMode(const Color(0xFFF5F5F5), const Color(0xFF3D3D3D)),
        shape: isCircleMedia ? BoxShape.circle : BoxShape.rectangle,
      ),
      alignment: Alignment.center,
      child: Icon(icon, size: mediaSize / 2, color: adaptedIconColor),
    );
  }

  /// 构建右侧组件(自定义/开关/箭头)
  Widget _buildTrailing() {
    // 自定义右侧组件优先
    if (trailing != null) return trailing!;

    // 开关组件
    if (switchValue != null) {
      return Switch(
        value: switchValue!,
        onChanged: isDisabled ? null : onSwitchChanged,
        activeColor: _adaptDarkMode(Colors.blue, Colors.blueAccent),
      );
    }

    // 右箭头
    if (showArrow) {
      return Icon(
        Icons.arrow_forward_ios,
        size: 16,
        color: _adaptDarkMode(const Color(0xFF999999), const Color(0xFF777777)),
      );
    }

    return const SizedBox.shrink();
  }

  /// 构建文本区域(标题+副标题)
  Widget _buildTextArea() {
    final adaptedTitleStyle = titleStyle ?? TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w500,
      color: _adaptDarkMode(Colors.black87, Colors.white70),
    );
    final adaptedSubtitleStyle = subtitleStyle ?? TextStyle(
      fontSize: 14,
      color: _adaptDarkMode(const Color(0xFF666666), const Color(0xFF999999)),
    );

    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          title,
          style: adaptedTitleStyle,
          maxLines: titleMaxLines,
          overflow: textOverflow,
        ),
        if (subtitle != null)
          Padding(
            padding: const EdgeInsets.only(top: 4.0),
            child: Text(
              subtitle!,
              style: adaptedSubtitleStyle,
              maxLines: subtitleMaxLines,
              overflow: textOverflow,
            ),
          ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    // 适配背景色(选中/禁用/默认)
    Color bgColor = _adaptDarkMode(Colors.white, const Color(0xFF2D2D2D));
    if (isSelected) {
      bgColor = _adaptDarkMode(selectedColor, const Color(0xFF3A5F88));
    } else if (isDisabled) {
      bgColor = _adaptDarkMode(disabledColor, const Color(0xFF333333));
    }

    // 适配分隔线颜色
    final adaptedDividerColor = _adaptDarkMode(dividerColor, const Color(0xFF444444));

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        GestureDetector(
          onTap: isDisabled ? null : onTap,
          child: Container(
            minHeight: minHeight,
            padding: padding,
            color: bgColor,
            child: Row(
              children: [
                // 左侧媒体组件
                _buildMedia(),
                const SizedBox(width: 12),
                // 中间文本区域(占满剩余空间)
                Expanded(child: _buildTextArea()),
                const SizedBox(width: 8),
                // 右侧组件
                _buildTrailing(),
              ],
            ),
          ),
        ),
        // 底部分隔线(最后一项不显示)
        if (showDivider)
          Container(
            height: dividerHeight,
            color: adaptedDividerColor,
            margin: EdgeInsets.only(left: mediaSize + 12 + padding.left),
          ),
      ],
    );
  }
}

四、三大高频场景落地示例(直接复制到项目可用)

场景 1:基础列表项(联系人列表 - 图标 + 标题 + 副标题)

适用场景:联系人、好友列表、消息列表等基础信息展示场景

dart

复制代码
// 联系人列表项
CommonListItemWidget(
  title: "张三",
  subtitle: "13800138000",
  icon: Icons.person,
  iconColor: Colors.blueAccent,
  mediaSize: 48,
  showArrow: true, // 显示右箭头,提示可点击进入详情
  onTap: () {
    // 跳转至联系人详情页
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => ContactDetailPage(name: "张三")),
    );
  },
  titleStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
  subtitleStyle: const TextStyle(fontSize: 14, color: Color(0xFF666666)),
  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  minHeight: 64,
);
场景 2:带徽章列表项(消息列表 - 图片 + 标题 + 徽章)

适用场景:消息通知、订单提醒、未读消息等需徽章提示的场景

dart

复制代码
// 消息列表项(带未读徽章)
CommonListItemWidget(
  title: "系统通知",
  subtitle: "您的订单已发货,请注意查收",
  imageUrl: "https://example.com/system-icon.png",
  mediaSize: 44,
  isCircleMedia: true,
  showArrow: true,
  onTap: () {
    // 跳转至消息详情页
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => MessageDetailPage()),
    );
  },
  // 自定义右侧组件:未读徽章
  trailing: Stack(
    alignment: Alignment.topRight,
    children: [
      Icon(
        Icons.arrow_forward_ios,
        size: 16,
        color: Color(0xFF999999),
      ),
      Container(
        width: 18,
        height: 18,
        decoration: BoxDecoration(
          color: Colors.redAccent,
          shape: BoxShape.circle,
        ),
        alignment: Alignment.center,
        child: Text(
          "3",
          style: TextStyle(fontSize: 12, color: Colors.white),
        ),
      ),
    ],
  ),
  placeholder: Container(
    width: 44,
    height: 44,
    decoration: BoxShape.circle,
    color: Color(0xFFF5F5F5),
  ),
);
场景 3:带开关列表项(设置页面 - 标题 + 开关)

适用场景:APP 设置页、功能开关、权限控制等需开关的场景

dart

复制代码
// 设置页面-消息通知开关
StatefulBuilder(
  builder: (context, setState) {
    bool _notificationEnabled = true;
    return CommonListItemWidget(
      title: "消息通知",
      subtitle: "接收APP的订单、活动等通知",
      icon: Icons.notifications,
      iconColor: Colors.orangeAccent,
      mediaSize: 40,
      switchValue: _notificationEnabled,
      onSwitchChanged: (value) {
        setState(() => _notificationEnabled = value);
        // 实际业务:保存开关状态到本地
        saveSwitchStatus("notification", value);
      },
      titleStyle: const TextStyle(fontSize: 16),
      subtitleStyle: const TextStyle(fontSize: 14, color: Color(0xFF666666)),
      isDisabled: false,
      showDivider: true,
      dividerColor: Color(0xFFF0F0F0),
    );
  },
);

五、核心封装技巧(复用成熟设计思路)

  1. 布局组合模式 :采用 "左侧媒体 + 中间文本 + 右侧组件" 的三段式布局,通过 Expanded 确保文本区域自适应,右侧组件灵活扩展
  2. 媒体优先级设计:网络图片 > 本地图片 > 图标,支持加载占位与失败降级,提升鲁棒性
  3. 交互状态聚合:整合选中、禁用、点击反馈状态,自动适配样式,无需外部手动管理状态与手势
  4. 右侧组件插槽化 :支持自定义 trailing 组件,同时内置箭头、开关等常用组件,兼顾灵活性与易用性
  5. 分隔线智能适配:分隔线位置自动适配左侧媒体尺寸,避免与左侧组件对齐混乱,符合视觉规范

六、避坑指南(解决 90% 开发痛点)

  1. 媒体组件优先级:当同时配置多个媒体参数时,仅优先级最高的生效(网络图片 > 本地图片 > 图标),避免重复配置导致混乱
  2. 图片加载优化 :网络图片必须配置 placeholdererrorWidget,避免网络波动导致的 UI 异常;本地图片需在 pubspec.yaml 中配置资源路径
  3. 文本溢出处理 :长文本场景需合理设置 titleMaxLinessubtitleMaxLines,建议副标题不超过 2 行,避免列表项高度异常
  4. 开关组件配置 :使用开关组件时必须同时配置 switchValueonSwitchChanged,否则开关无法正常交互
  5. 分隔线对齐 :分隔线默认与文本区域左对齐,若需全屏分隔线,可设置 margin: EdgeInsets.zero 覆盖默认边距
  6. 深色模式兼容 :自定义颜色(如图标色、文本色)需通过 _adaptDarkMode 方法适配,避免深色模式下颜色冲突

https://openharmonycrossplatform.csdn.net/content

相关推荐
似水流年QC16 小时前
深入探索 WebHID:Web 标准下的硬件交互实现
前端·交互·webhid
kirk_wang20 小时前
Flutter 导航锁踩坑实录:从断言失败到类型转换异常
前端·javascript·flutter
往来凡尘21 小时前
Flutter运行iOS26真机的两个问题
flutter·ios
yangSnowy1 天前
webman框架虚拟数据填充fakerphp/faker插件的使用
php
简鹿视频1 天前
视频转mp4格式具体作步骤
ffmpeg·php·音视频·实时音视频
liebe1*11 天前
第十一章 密码学
服务器·密码学·php
yfmingo1 天前
flutter项目大量使用.obs会导致项目性能极度下降吗
flutter
山璞1 天前
Flutter3.32 中使用 webview4.13 与 vue3 项目的 h5 页面通信,以及如何调试
前端·flutter