DataTable数据表格 - OpenHarmony PC端Flutter
本文介绍了Flutter中DataTable组件的使用方法和企业级应用实现。主要内容包括: DataTable基础构建:通过定义列(columns)和行(rows)创建结构化表格,支持文本、图标等单元格内容展示。 核心功能实现: 排序功能:通过DataColumn的onSort回调实现列排序 行选择:利用DataRow的selected属性和onSelectChanged回调实现单选/多选 高级应
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
案例概述
本案例展示如何使用 DataTable 创建数据表格,包括排序、选择等功能。数据表格是企业应用中最常见的数据展示方式,用于展示结构化的表格数据。在 Flutter 中,DataTable 组件提供了一个强大而灵活的表格实现,支持多种交互功能,如排序、行选择、自定义样式等。
在实际应用中,数据表格通常需要处理大量数据,因此性能优化、缓存策略、分页加载等都是重要的考虑因素。此外,对于 PC 端应用,数据表格的响应式设计、键盘导航、无障碍支持等也是必不可少的。本案例将详细介绍如何构建一个功能完整、高效易用的数据表格系统。
核心概念
1. DataTable 组件
DataTable 是 Flutter Material 设计库中提供的表格组件,用于展示结构化的表格数据。它由列(columns)和行(rows)组成,每一行包含多个单元格(cells)。DataTable 支持多种交互功能,包括行选择、列排序、自定义样式等。在使用 DataTable 时,需要定义表格的列结构和行数据,然后 DataTable 会自动根据这些定义渲染表格。
DataTable 的主要特点是提供了一个标准的表格布局,具有清晰的列标题、行分隔线和单元格内容。它支持响应式设计,可以根据屏幕宽度自动调整列宽。此外,DataTable 还提供了丰富的自定义选项,如行颜色、行高、列间距等,使开发者能够灵活地定制表格外观。
2. DataColumn 列定义
DataColumn 用于定义表格的列结构,每个 DataColumn 代表表格中的一列。在 DataColumn 中,可以指定列的标签(label)、是否支持排序(onSort)、标签对齐方式等。当用户点击支持排序的列标题时,DataColumn 会触发 onSort 回调,开发者可以在回调中实现排序逻辑。
列定义是构建表格的基础,它决定了表格的结构和功能。通过合理设计列定义,可以使表格更加清晰易读。在实际应用中,列定义通常包括数据字段名、显示标签、排序功能、列宽等信息。
3. DataRow 行定义与 DataCell 单元格
DataRow 用于定义表格的行,每个 DataRow 包含多个 DataCell(单元格)。DataRow 支持行选择功能,通过 selected 属性可以标记行的选中状态,通过 onSelectChanged 回调可以处理行选择事件。
DataCell 是表格中的最小单位,用于展示单个数据项。DataCell 可以包含任何 Widget,如文本、图标、按钮等,这使得表格具有很高的灵活性。通过合理使用 DataCell,可以在表格中展示复杂的数据和交互元素。
代码详解
1. 基础表格构建
创建一个基础的 DataTable 需要定义两个主要部分:列定义(columns)和行数据(rows)。列定义决定了表格的结构,行数据提供了表格要展示的内容。在下面的例子中,我们创建了一个包含 ID、姓名和邮箱三列的表格。
DataTable(
columns: [
DataColumn(label: Text('ID')),
DataColumn(label: Text('姓名')),
DataColumn(label: Text('邮箱')),
],
rows: _data.map((item) {
return DataRow(
cells: [
DataCell(Text(item['id'].toString())),
DataCell(Text(item['name'])),
DataCell(Text(item['email'])),
],
);
}).toList(),
)
这个基础表格会自动渲染列标题、行分隔线和单元格内容。每个 DataRow 对应数据列表中的一项,每个 DataCell 对应该项中的一个字段。
2. 排序功能实现
表格排序是数据展示中的常见需求。通过在 DataColumn 中定义 onSort 回调,可以实现列排序功能。当用户点击列标题时,会触发 onSort 回调,开发者可以在回调中对数据进行排序。
DataColumn(
label: Text('姓名'),
onSort: (columnIndex, ascending) {
setState(() {
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
_data.sort((a, b) => ascending
? a['name'].compareTo(b['name'])
: b['name'].compareTo(a['name']));
});
},
)
排序功能需要维护当前排序列的索引和排序方向(升序或降序)。当用户再次点击已排序的列时,排序方向会自动反转,提供更好的用户体验。
3. 行选择与多选
行选择功能允许用户选择一行或多行数据,这在批量操作场景中非常有用。通过 DataRow 的 selected 属性和 onSelectChanged 回调,可以实现行选择功能。
DataRow(
selected: _selectedRows.contains(index),
onSelectChanged: (selected) {
setState(() {
if (selected ?? false) {
_selectedRows.add(index);
} else {
_selectedRows.remove(index);
}
});
},
cells: [...],
)
在这个实现中,我们使用一个 Set 来存储已选中的行索引。当用户点击行的选择框时,会触发 onSelectChanged 回调,根据选择状态添加或移除该行的索引。
高级话题:DataTable 的企业级应用
在实际的企业应用中,数据表格通常需要处理更复杂的场景,如动态列定义、大数据集分页、高级搜索过滤、批量操作、数据导出等。本部分介绍如何在 DataTable 的基础上,实现这些高级功能,使其能够满足企业级应用的需求。
1. 动态列定义与多列排序
动态列定义是指表格的列结构不是硬编码的,而是根据数据或配置动态生成的。这在需要展示不同类型数据的应用中非常有用。多列排序则允许用户按多个列进行排序,提供更灵活的数据查看方式。
List<DataColumn> buildColumns(List<String> columnNames) {
return columnNames.map((name) {
return DataColumn(
label: Text(name),
onSort: (columnIndex, ascending) {
_handleSort(columnIndex, ascending);
},
);
}).toList();
}
void _handleSort(int columnIndex, bool ascending) {
setState(() {
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
_data.sort((a, b) {
final aValue = a.values.toList()[columnIndex];
final bValue = b.values.toList()[columnIndex];
return ascending
? aValue.toString().compareTo(bValue.toString())
: bValue.toString().compareTo(aValue.toString());
});
});
}
2. 表格分页与虚拟滚动
class _DataTableSource extends DataTableSource {
final List<Map<String, dynamic>> data;
final int pageSize = 10;
DataRow? getRow(int index) {
if (index >= data.length) return null;
final item = data[index];
return DataRow(
cells: [
DataCell(Text(item['id'].toString())),
DataCell(Text(item['name'])),
DataCell(Text(item['email'])),
],
);
}
int get rowCount => data.length;
bool get isRowCountApproximate => false;
int get selectedRowCount => 0;
}
PaginatedDataTable(
header: Text('用户列表'),
columns: [...],
source: _DataTableSource(_data),
rowsPerPage: 10,
onPageChanged: (page) => print('页码: $page'),
)
3. 表格导出与数据持久化
void exportToCSV() {
final buffer = StringBuffer();
// 添加表头
buffer.writeln(_columns.map((col) => col.label).join(','));
// 添加数据行
for (var row in _data) {
buffer.writeln(row.values.join(','));
}
// 保存文件
_saveFile('export.csv', buffer.toString());
}
void exportToJSON() {
final json = jsonEncode(_data);
_saveFile('export.json', json);
}
Future<void> _saveFile(String filename, String content) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$filename');
await file.writeAsString(content);
}
4. 表格搜索、过滤与高级查询
List<Map<String, dynamic>> get _filteredData {
return _data.where((item) {
final matchesSearch = _searchQuery.isEmpty ||
item['name'].toLowerCase().contains(_searchQuery.toLowerCase()) ||
item['email'].toLowerCase().contains(_searchQuery.toLowerCase());
final matchesFilter = _selectedFilter == null ||
item['status'] == _selectedFilter;
return matchesSearch && matchesFilter;
}).toList();
}
// 高级查询示例
List<Map<String, dynamic>> advancedQuery({
required String searchTerm,
required String? filterStatus,
required String sortBy,
required bool ascending,
}) {
var result = _data.where((item) {
return item['name'].toLowerCase().contains(searchTerm.toLowerCase());
}).toList();
if (filterStatus != null) {
result = result.where((item) => item['status'] == filterStatus).toList();
}
result.sort((a, b) {
final aVal = a[sortBy];
final bVal = b[sortBy];
return ascending ? aVal.compareTo(bVal) : bVal.compareTo(aVal);
});
return result;
}
5. 表格行操作与批量处理
DataRow(
selected: _selectedRows.contains(index),
onSelectChanged: (selected) {
setState(() {
if (selected ?? false) {
_selectedRows.add(index);
} else {
_selectedRows.remove(index);
}
});
},
cells: [
DataCell(Text(item['name'])),
DataCell(
Row(
children: [
IconButton(
icon: Icon(Icons.edit),
onPressed: () => _showEditDialog(item),
),
IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteRow(item),
),
IconButton(
icon: Icon(Icons.more_vert),
onPressed: () => _showContextMenu(item),
),
],
),
),
],
)
void _batchDelete() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('批量删除'),
content: Text('确定删除 ${_selectedRows.length} 条记录吗?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消')),
TextButton(
onPressed: () {
setState(() {
for (int i = _selectedRows.length - 1; i >= 0; i--) {
_data.removeAt(_selectedRows[i]);
}
_selectedRows.clear();
});
Navigator.pop(context);
},
child: Text('删除'),
),
],
),
);
}
6. 表格样式自定义与主题适配
DataTable(
dataRowColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return Colors.blue.shade100;
}
if (states.contains(MaterialState.hovered)) {
return Colors.grey.shade100;
}
return Colors.white;
}),
headingRowColor: MaterialStateProperty.all(Colors.blue.shade50),
headingRowHeight: 56,
dataRowHeight: 48,
columnSpacing: 16,
columns: [...],
rows: [...],
)
7. 表格响应式设计与移动适配
Widget _buildTable() {
final width = MediaQuery.of(context).size.width;
final isWideScreen = width > 1200;
final isTablet = width > 600 && width <= 1200;
if (isWideScreen) {
return _buildFullDataTable();
} else if (isTablet) {
return _buildCompactDataTable();
} else {
return _buildCardListView();
}
}
Widget _buildCardListView() {
return ListView.builder(
itemCount: _data.length,
itemBuilder: (context, index) {
final item = _data[index];
return Card(
margin: EdgeInsets.all(8),
child: ListTile(
title: Text(item['name']),
subtitle: Text(item['email']),
trailing: PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(child: Text('编辑')),
PopupMenuItem(child: Text('删除')),
],
),
),
);
},
);
}
8. 表格的键盘导航与快捷键
Focus(
onKey: (node, event) {
if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) {
setState(() => _selectedRowIndex = (_selectedRowIndex + 1) % _data.length);
return KeyEventResult.handled;
}
if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) {
setState(() => _selectedRowIndex = (_selectedRowIndex - 1 + _data.length) % _data.length);
return KeyEventResult.handled;
}
if (event.isKeyPressed(LogicalKeyboardKey.enter)) {
_editRow(_data[_selectedRowIndex]);
return KeyEventResult.handled;
}
if (event.isKeyPressed(LogicalKeyboardKey.delete)) {
_deleteRow(_data[_selectedRowIndex]);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: DataTable(...),
)
9. 表格的无障碍支持与屏幕阅读器
DataTable(
columns: [
DataColumn(
label: Semantics(
label: '用户 ID 列',
enabled: true,
child: Text('ID'),
),
),
DataColumn(
label: Semantics(
label: '用户姓名列',
enabled: true,
child: Text('姓名'),
),
),
],
rows: _data.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return DataRow(
cells: [
DataCell(
Semantics(
label: '第 ${index + 1} 行,ID: ${item['id']}',
child: Text(item['id'].toString()),
),
),
DataCell(
Semantics(
label: '第 ${index + 1} 行,姓名: ${item['name']}',
child: Text(item['name']),
),
),
],
);
}).toList(),
)
10. 表格的单元测试与集成测试
void main() {
group('DataTable Tests', () {
test('表格排序功能', () {
final data = [
{'id': 1, 'name': '李四'},
{'id': 2, 'name': '张三'},
];
data.sort((a, b) => a['name'].compareTo(b['name']));
expect(data[0]['name'], '张三');
});
test('表格过滤功能', () {
final data = [
{'id': 1, 'name': '张三', 'status': 'active'},
{'id': 2, 'name': '李四', 'status': 'inactive'},
];
final filtered = data.where((item) => item['status'] == 'active').toList();
expect(filtered.length, 1);
});
test('表格行选择', () {
final selectedRows = <int>{};
selectedRows.add(0);
selectedRows.add(1);
expect(selectedRows.length, 2);
});
});
}
// 集成测试示例
void main() {
testWidgets('DataTable 集成测试', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// 验证表格存在
expect(find.byType(DataTable), findsOneWidget);
// 验证行数
expect(find.byType(DataRow), findsWidgets);
// 点击排序
await tester.tap(find.text('姓名'));
await tester.pumpAndSettle();
// 验证排序结果
expect(find.text('张三'), findsOneWidget);
});
}
通过这些企业级技巧,你可以构建出功能完整、高效、易用的数据表格系统。
更多推荐
所有评论(0)