在 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 枚举分类:覆盖场景 + 便于扩展
通过RightWidgetType和LeadingShape两个枚举,将右侧组件和左侧图片形状分类,既覆盖了高频场景,又便于后续扩展(如新增 "小红点" 右侧类型、"菱形" 图片形状)。
4.2 优先级设计:避免样式冲突
明确 "左侧图标优先级高于图片",当同时传入leadingIcon和leadingImage时,只会显示图标,避免样式混乱;同时通过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 稳定性
- 图片加载时显示加载指示器,加载失败显示错误图标,避免空白占位。
- 文本溢出自动截断,避免文字超出屏幕导致布局错乱。
- 开关回调、点击事件均做了非空判断,避免空指针异常。
五、避坑指南(实际开发必看)
- 分割线缩进适配 :当左侧有图标 / 图片时,分割线缩进建议设置为
leadingSize + leadingSpacing,确保分割线与标题对齐,视觉更统一。 - 图片尺寸控制 :左侧图片尺寸不宜过大(建议 40-60px),避免挤压中间标题区域;同时设置
fit: BoxFit.cover,保证图片比例正常。 - 圆角与裁剪 :当设置
borderRadius时,需用ClipRRect包裹内容,否则背景色会溢出圆角;同时InkWell的borderRadius要与列表项一致,水波纹才会贴合圆角。 - 开关状态同步 :使用
switchBtn类型时,switchValue需与父组件状态绑定(如useState/Provider),否则开关状态会错乱。 - 长列表性能 :在
ListView.builder中使用时,确保列表项是 "懒加载" 的,避免一次性创建过多组件;如果列表项背景色统一,可设置ListView的cacheExtent优化滚动性能。
六、进阶拓展场景(提升组件上限)
- 新增小红点功能 :扩展
RightWidgetType为dot,实现右侧小红点提醒(无需数字),适配 "仅提醒未查看" 场景。 - 支持渐变背景 :新增
gradientBackground参数,支持列表项渐变背景,适配更复杂的设计需求。 - 左侧多图标 :支持左侧显示多个图标(如 "推荐" 标签 + 主图标),通过
List<Widget>? leadingIcons参数扩展。 - 副标题多行显示 :新增
subtitleMaxLines参数,支持副标题多行显示,适配长文本场景。 - 动画效果 :添加
onTap时的缩放动画,提升交互反馈质感。
总结
本文封装的ListItemWidget核心优势在于 "通用、灵活、统一":
- 通用:覆盖设置页、消息列表、商品列表、联系人等 90%+ 场景,无需重复编码。
- 灵活:样式、交互、布局均可自定义,适配不同设计规范和业务需求。
- 统一:通过统一的组件封装,保证 APP 内所有列表项样式一致,提升产品质感。