如何在Flutter中创建支持单元格合并的数据表(适配API数据)
Flutter DataTable 单元格合并实现
需求
需要实现指定样式的DataTable:核心要求为**前两列(Zone、Point)和最后两列(Accessories、Remarks)**的单元格,根据中间警员数据的行数进行跨行合并。
当前已通过API获取数据并实现基础DataTable,但缺少单元格合并功能,现有代码如下:
DataTable( columns: const [ DataColumn(label: Text("Zone")), DataColumn(label: Text("Point")), DataColumn(label: Text("Designation")), DataColumn(label: Text("Buckle Number")), DataColumn(label: Text("Police Name")), DataColumn(label: Text("District")), DataColumn(label: Text("Police Station Name")), DataColumn(label: Text("Age")), DataColumn(label: Text("Gender")), DataColumn(label: Text("Number")), DataColumn(label: Text("Accessories")), DataColumn(label: Text("Remarks")) ], rows: controller.eventAssignmentModel.value?.pointAssignments![index] .assignedPoliceList! .asMap() .map((index4, police) => MapEntry( index4, DataRow(cells: [ DataCell(Text(controller.eventAssignmentModel.value!.pointAssignments![index].zoneName!)), DataCell(Text(controller.eventAssignmentModel.value!.pointAssignments![index].pointName!)), DataCell(Text(police.designation ?? '')), DataCell(Text(police.buckleNumber ?? '')), DataCell(Text(police.policeName ?? '')), DataCell(Text(police.district ?? '')), DataCell(Text(police.policeStationName ?? '')), DataCell(Text(police.age ?? '')), DataCell(Text(police.gender ?? '')), DataCell(Text(police.number ?? '')), DataCell(Text(controller.eventAssignmentModel.value!.pointAssignments![index].pointAccessories!)), DataCell(Text(controller.eventAssignmentModel.value!.pointAssignments![index].pointRemarks!)), ]))) .values .toList() ?? const [], ),
实现方案
Flutter原生DataTable不支持单元格合并,推荐以下两种实现方式:
方案一:使用第三方包(推荐)
采用data_table_2包,它支持单元格跨行合并,且自带自适应、滚动等功能。
- 添加依赖到
pubspec.yaml:
dependencies: data_table_2: ^2.5.0
- 修改代码实现合并:
import 'package:data_table_2/data_table_2.dart'; // 构建带合并的表格 DataTable2( columnSpacing: 12, horizontalMargin: 12, minWidth: 600, columns: const [ DataColumn2(label: Text("Zone"), size: ColumnSize.S), DataColumn2(label: Text("Point"), size: ColumnSize.S), DataColumn2(label: Text("Designation"), size: ColumnSize.M), DataColumn2(label: Text("Buckle Number"), size: ColumnSize.M), DataColumn2(label: Text("Police Name"), size: ColumnSize.M), DataColumn2(label: Text("District"), size: ColumnSize.M), DataColumn2(label: Text("Police Station Name"), size: ColumnSize.M), DataColumn2(label: Text("Age"), size: ColumnSize.S), DataColumn2(label: Text("Gender"), size: ColumnSize.S), DataColumn2(label: Text("Number"), size: ColumnSize.M), DataColumn2(label: Text("Accessories"), size: ColumnSize.L), DataColumn2(label: Text("Remarks"), size: ColumnSize.L), ], rows: _buildMergedRows(controller, index), ) // 生成带合并单元格的行列表 List<DataRow> _buildMergedRows(YourController controller, int index) { final point = controller.eventAssignmentModel.value?.pointAssignments![index]; if (point == null || point.assignedPoliceList == null) return []; final policeList = point.assignedPoliceList!; final rowCount = policeList.length; final rows = <DataRow>[]; for (int i = 0; i < rowCount; i++) { final police = policeList[i]; rows.add(DataRow( cells: [ // Zone列:跨行合并,仅第一行显示内容 DataCell( Text(point.zoneName!), placeholder: i != 0, rowSpan: rowCount, ), // Point列:同理合并 DataCell( Text(point.pointName!), placeholder: i != 0, rowSpan: rowCount, ), // 中间列正常渲染警员数据 DataCell(Text(police.designation ?? '')), DataCell(Text(police.buckleNumber ?? '')), DataCell(Text(police.policeName ?? '')), DataCell(Text(police.district ?? '')), DataCell(Text(police.policeStationName ?? '')), DataCell(Text(police.age ?? '')), DataCell(Text(police.gender ?? '')), DataCell(Text(police.number ?? '')), // Accessories列:跨行合并 DataCell( Text(point.pointAccessories!), placeholder: i != 0, rowSpan: rowCount, ), // Remarks列:跨行合并 DataCell( Text(point.pointRemarks!), placeholder: i != 0, rowSpan: rowCount, ), ], )); } return rows; }
方案二:原生Table组件实现
如果不想依赖第三方包,可使用Flutter原生Table组件手动实现合并:
Table( border: TableBorder.all(color: Colors.grey), children: _buildCustomTableRows(controller, index), ) List<TableRow> _buildCustomTableRows(YourController controller, int index) { final point = controller.eventAssignmentModel.value?.pointAssignments![index]; if (point == null || point.assignedPoliceList == null) return []; final policeList = point.assignedPoliceList!; final rowCount = policeList.length; final rows = <TableRow>[]; for (int i = 0; i < rowCount; i++) { final police = policeList[i]; rows.add(TableRow( children: [ // Zone列合并:仅第一行渲染内容,其余行留空 i == 0 ? TableCell( verticalAlignment: TableCellVerticalAlignment.fill, rowSpan: rowCount, child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(point.zoneName!), ), ) : const TableCell(child: SizedBox()), // Point列合并 i == 0 ? TableCell( verticalAlignment: TableCellVerticalAlignment.fill, rowSpan: rowCount, child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(point.pointName!), ), ) : const TableCell(child: SizedBox()), // 中间警员信息列 TableCell( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(police.designation ?? ''), ), ), TableCell( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(police.buckleNumber ?? ''), ), ), TableCell( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(police.policeName ?? ''), ), ), TableCell( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(police.district ?? ''), ), ), TableCell( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(police.policeStationName ?? ''), ), ), TableCell( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(police.age ?? ''), ), ), TableCell( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(police.gender ?? ''), ), ), TableCell( child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(police.number ?? ''), ), ), // Accessories列合并 i == 0 ? TableCell( verticalAlignment: TableCellVerticalAlignment.fill, rowSpan: rowCount, child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(point.pointAccessories!), ), ) : const TableCell(child: SizedBox()), // Remarks列合并 i == 0 ? TableCell( verticalAlignment: TableCellVerticalAlignment.fill, rowSpan: rowCount, child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), child: Text(point.pointRemarks!), ), ) : const TableCell(child: SizedBox()), ], )); } return rows; }
注意事项
- 第三方包方案开发效率更高,适合复杂表格场景;
- 原生方案需自行处理滚动、响应式等细节,但无额外依赖;
- 核心逻辑:同一个
point下的所有警员行,合并列的第一个单元格设置rowSpan为警员总数,其余行对应位置留空。
内容的提问来源于stack exchange,提问作者Parthish Joshi
相关产品推荐
相关产品推荐

