欢迎加入开源鸿蒙跨平台社区: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);
  });
}

通过这些企业级技巧,你可以构建出功能完整、高效、易用的数据表格系统。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐