Flutter DataTable实现左滑行显示删除按钮点击删除功能
DataTable左滑行显示删除按钮实现方案
问题说明
项目中使用Flutter DataTable组件时,需要实现交互:向左滑动表格某一行时,该行右侧呼出删除图标,点击图标即可删除对应行,效果参考如下:
此前尝试在onSelectChanged回调中添加SwipeActionCell()组件实现功能,但运行未达预期,现有实现代码如下:
Expanded( child: SizedBox( child: SingleChildScrollView( child: DataTable( showCheckboxColumn: false, columnSpacing: unitWidthValue * 0.021, columns: [ DataColumn( label: Center( child: Text('编号', style: TextStyle( fontSize: unitWidthValue * tableHeaderTextSizePercent, fontFamily: " SF Pro Display", fontWeight: FontWeight.w300 ), ), ) ), DataColumn( label: Center( child: Text('名称', style: TextStyle( fontSize: unitWidthValue * tableHeaderTextSizePercent, fontFamily: " SF Pro Display", fontWeight: FontWeight.w300 ), ), ) ), DataColumn( label: Center( child: Text('日期', style: TextStyle( fontSize: unitWidthValue * tableHeaderTextSizePercent, fontFamily: " SF Pro Display", fontWeight: FontWeight.w300 ), ), ) ), const DataColumn(label: Text('')), ], rows: widget.persons .map((person) => DataRow( cells: <DataCell>[ DataCell(Text(person.number)), DataCell(SizedBox( width: unitWidthValue * 0.35, child: Text(person.sampleNumber, style: TextStyle( fontSize: unitWidthValue * 0.035, fontFamily: "SF Pro Display" ), ), )), DataCell(Text(person.name, style: TextStyle( fontSize: unitWidthValue * 0.035, fontFamily: "SF Pro Display" ) )), DataCell(Padding( padding: EdgeInsets.only(top: unitHeightValue * 0.02), child: Column( children: [ if (person.status == personStatus.recorded || person.status == personStatus.associate) const Icon(Icons.save, color: Color(0xFF0061B0),) else if (person.status == personStatus.transmitted) const Icon(Icons.check_circle, color: Color(0xFF34C759),) else const Icon(Icons.warning, color: Color(0xFFFF9500),) ], ), )) ], onSelectChanged: (bool? isDispalyData) { setState(() { Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation1, animation2) => DisplayData(person, isDisplayData: true), transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ), ); }); }, )) .toList() ), ), )) ], ), )
问题根因
onSelectChanged是DataRow的点击事件回调,仅用于处理行被点击后的业务逻辑,这个位置无法插入、渲染UI组件,写在这里的SwipeActionCell根本不会被挂载到视图树,自然不会生效。
实现方案
核心思路:将滑动交互组件放在DataCell的视图层级中,把整行内容作为滑动组件的子节点,统一承载滑动删除、点击跳转的手势逻辑,不要把UI组件写到事件回调里。
方案1:使用flutter_swipe_action_cell(和参考图效果完全一致)
该组件支持左滑后停留在显示操作按钮的状态,和给出的参考效果匹配度最高。
- 第一步:添加依赖,在项目
pubspec.yaml中写入依赖后执行flutter pub get安装:
dependencies: flutter_swipe_action_cell: ^3.1.3
- 第二步:改造DataRow的单元格逻辑,将整行内容整合到第一个DataCell中,外层包裹
SwipeActionCell,剩余3个单元格设置为空白占位即可,核心改造代码如下:
// 先抽离状态图标构建逻辑,减少代码嵌套 Widget _buildStatusIcon(personStatus status) { if (status == personStatus.recorded || status == personStatus.associate) { return const Icon(Icons.save, color: Color(0xFF0061B0)); } else if (status == personStatus.transmitted) { return const Icon(Icons.check_circle, color: Color(0xFF34C759)); } else { return const Icon(Icons.warning, color: Color(0xFFFF9500)); } } // DataTable的rows部分改造 rows: widget.persons .map((person) => DataRow( // 保留原有点击行跳转逻辑 onSelectChanged: (bool? isDisplayData) { setState(() { Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation1, animation2) => DisplayData(person, isDisplayData: true), transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ), ); }); }, cells: <DataCell>[ DataCell( SizedBox( // 宽度设置为表格行可用总宽度,保证滑动区域覆盖整行 width: MediaQuery.of(context).size.width - 32, // 可根据自己的布局padding调整 child: SwipeActionCell( key: ObjectKey(person), // 每行设置唯一key,避免列表状态错乱 // 左侧滑动动作如果不需要可以不写,只保留右侧trailingActions trailingActions: <SwipeAction>[ SwipeAction( title: "删除", style: const TextStyle(color: Colors.white, fontSize: 16), color: Colors.red, icon: const Icon(Icons.delete, color: Colors.white), onTap: (CompletionHandler handler) async { // 执行删除逻辑 setState(() { widget.persons.remove(person); }); // 收回滑动操作区域 handler(true); }, ), ], // 整行原有内容按列横向排列,宽度和表头对齐即可 child: Row( children: [ // 第一列:编号 SizedBox( width: unitWidthValue * 0.18, child: Text(person.number, style: TextStyle( fontSize: unitWidthValue * 0.035, fontFamily: "SF Pro Display" ), ), ), // 第二列:样本编号 SizedBox( width: unitWidthValue * 0.35, child: Text(person.sampleNumber, style: TextStyle( fontSize: unitWidthValue * 0.035, fontFamily: "SF Pro Display" ), ), ), // 第三列:姓名 SizedBox( width: unitWidthValue * 0.2, child: Text(person.name, style: TextStyle( fontSize: unitWidthValue * 0.035, fontFamily: "SF Pro Display" ), ), ), // 第四列:状态图标 Padding( padding: EdgeInsets.only(top: unitHeightValue * 0.02), child: _buildStatusIcon(person.status), ) ], ), ), ), ), // 剩余三个单元格用空白组件占位 const DataCell(SizedBox.shrink()), const DataCell(SizedBox.shrink()), const DataCell(SizedBox.shrink()), ], )) .toList()
- 手势冲突处理:如果出现滑动和点击跳转冲突,可以给
SwipeActionCell设置opensOnScroll: true属性,只有横向滑动距离超过阈值时才触发滑动动作,否则正常响应点击跳转。
方案2:无第三方依赖,用官方Dismissible实现
如果不想引入第三方库,可以用Flutter自带的Dismissible组件实现滑动删除,缺点是交互为滑动超过阈值直接触发删除,无法停留在显示删除按钮的状态。
只需要把方案1里的SwipeActionCell替换为Dismissible即可:
Dismissible( key: ObjectKey(person), direction: DismissDirection.endToStart, // 仅允许从右向左滑动 // 滑动时露出的红色删除背景 background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 24), child: const Icon(Icons.delete, color: Colors.white), ), // 滑动完成后触发删除 onDismissed: (direction) { setState(() { widget.persons.remove(person); }); }, child: // 原有整行内容Row,和方案1一致 )
注意事项
- 每个滑动行必须设置唯一
key,直接用对应数据对象作为ObjectKey的值即可,避免列表更新时状态错乱 - 需要手动调整Row内每列的宽度,和表头列宽、列间距匹配,保证内容对齐
- 如果DataTable设置了横向滚动,需要给包裹滑动组件的SizedBox设置足够的宽度,避免内容溢出
内容的提问来源于stack exchange,提问作者compte profe
相关产品推荐
相关产品推荐

