Flutter DataGrid,如何进行数据分组更简单?

253 阅读8分钟

原文:xuanhu.info/projects/it…

Flutter DataGrid,如何进行数据分组更简单?

在现代移动应用开发中,数据表格是展示结构化信息的重要组件。Syncfusion 的 Flutter DataGrid(SfDataGrid)提供了强大的数据分组功能,允许开发者根据特定条件对数据进行组织和分类,形成层次化结构,极大提升了数据的可读性和用户体验。

数据分组的核心概念与价值

数据分组是一种将具有相同特征或特定字段值的数据记录组织在一起的技术。在 SfDataGrid 中,分组功能可以:

  • 提高数据可读性:通过将相关记录归类,减少视觉混乱

  • 增强数据分析能力:用户可以快速识别模式和趋势

  • 优化用户交互:支持展开/折叠操作,便于导航大量数据

  • 提供摘要信息:每个分组可以显示汇总数据,如计数、求和等

当启用分组功能时,SfDataGrid 会为每个分组创建一个标题摘要行(CaptionSummaryRow),显示在该组的顶部,包含该组的标题摘要值。

实现数据分组的基本步骤

1. 创建数据模型

首先定义数据模型,这是绑定到 DataGrid 的基础:


class Employee {

Employee(this.id, this.name, this.designation, this.salary);

final int id;

final String name;

final String designation;

final int salary;

}

2. 准备数据源

创建数据集合并在 initState() 中初始化:


List<Employee> employees = <Employee>[];

  


@override

void initState() {

super.initState();

employees = getEmployeeData();

// 其他初始化代码

}

3. 创建 DataGridSource

DataGridSource 是 SfDataGrid 和数据模型之间的桥梁,负责提供数据和处理分组逻辑:


class EmployeeDataSource extends DataGridSource {

EmployeeDataSource({required List<Employee> employeeData}) {

dataGridRows = employeeData

.map<DataGridRow>((e) => DataGridRow(cells: [

DataGridCell<int>(columnName: 'ID', value: e.id),

DataGridCell<String>(columnName: 'Name', value: e.name),

DataGridCell<String>(columnName: 'Designation', value: e.designation),

DataGridCell<double>(columnName: 'Salary', value: e.salary),

]))

.toList();

}

List<DataGridRow> dataGridRows = [];

@override

List<DataGridRow> get rows => dataGridRows;

@override

DataGridRowAdapter buildRow(DataGridRow row) {

return DataGridRowAdapter(

cells: row.getCells().map<Widget>((e) {

return Container(

alignment: Alignment.center,

padding: EdgeInsets.all(8),

child: Text(e.value.toString()),

);

}).toList());

}

}

4. 启用分组功能

通过 DataGridSource 的 addColumnGroup 方法启用列分组:


@override

void initState() {

super.initState();

employees = getEmployeeData();

employeeDataSource = EmployeeDataSource(employeeData: employees);

// 添加列分组 - 按职位分组并排序

employeeDataSource.addColumnGroup(

ColumnGroup(name: 'Designation', sortGroupRows: true)

);

}

ColumnGroup 对象包含两个主要属性:

  • name: 要分组的 GridColumn 的列名

  • sortGroupRows: 确定是否按升序对分组列进行排序

5. 配置 SfDataGrid 组件

在 UI 中配置 SfDataGrid 组件:


@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(title: const Text('员工数据表')),

body: SfDataGrid(

source: employeeDataSource,

allowExpandCollapseGroup: true, // 允许展开/折叠分组

columns: <GridColumn>[

GridColumn(

columnName: 'ID',

label: Container(

padding: EdgeInsets.all(8),

alignment: Alignment.center,

child: Text('ID')

)

),

GridColumn(

columnName: 'Name',

label: Container(child: Text('姓名'))

),

GridColumn(

columnName: 'Designation',

label: Container(child: Text('职位', overflow: TextOverflow.ellipsis))

),

GridColumn(

columnName: 'Salary',

label: Container(child: Text('薪资'))

),

]

)

);

}

自定义分组标题显示

默认情况下,DataGrid 不会显示分组的标题摘要值。要显示这些值,需要重写 DataGridSource.buildGroupCaptionCellWidget 方法:


@override

Widget? buildGroupCaptionCellWidget(

RowColumnIndex rowColumnIndex, String summaryValue) {

return Container(

padding: EdgeInsets.symmetric(horizontal: 12, vertical: 15),

child: Text(summaryValue) // 显示分组摘要值

);

}

此方法接收标题摘要值作为参数,并返回包含摘要值的 widget。

分组操作管理

添加分组

如前面所示,使用 addColumnGroup 方法添加分组:


employeeDataSource.addColumnGroup(

ColumnGroup(name: 'Salary', sortGroupRows: false)

);

移除分组

要移除特定列的分组,使用 removeColumnGroup 方法:


ColumnGroup? group = employeeDataSource.groupedColumns

.firstWhereOrNull((element) => element.name == 'Salary');

if (group != null) {

employeeDataSource.removeColumnGroup(group);

}

清除所有分组

要一次性清除所有分组,调用 clearColumnGroups 方法:


employeeDataSource.clearColumnGroups();

多级分组实现

SfDataGrid 支持对多个列进行分组,创建层次化的树状结构。数据首先根据添加到 DataGridSource.addColumnGroup 属性的第一列进行分组,随后添加的每个新列都会在现有分组的基础上进行进一步分组。


@override

void initState() {

super.initState();

employees = getEmployeeData();

employeeDataSource = EmployeeDataSource(employeeData: employees);

// 添加多级分组

employeeDataSource.addColumnGroup(ColumnGroup(name: 'Designation', sortGroupRows: true));

employeeDataSource.addColumnGroup(ColumnGroup(name: 'Salary', sortGroupRows: false));

}

这种多级分组功能使您可以创建复杂的数据层次结构,例如先按部门分组,然后在每个部门内按薪资范围分组。

分组交互回调

SfDataGrid 提供了多个回调函数来通知分组的不同阶段:

GroupExpanding 回调

当分组即将展开时触发,可以返回 false 来阻止展开操作:


SfDataGrid(

source: employeeDataSource,

groupExpanding: (group) {

print('分组展开: ${group.key}');

print('分组级别: ${group.groupLevel}');

return true; // 返回 true 允许展开,false 阻止展开

},

// 其他配置...

)

GroupExpanded 回调

当分组完全展开后触发:


SfDataGrid(

source: employeeDataSource,

groupExpanded: (group) {

// 分组已展开后的处理逻辑

},

// 其他配置...

)

GroupCollapsing 回调

当分组即将折叠时触发,可以返回 false 来阻止折叠操作:


SfDataGrid(

source: employeeDataSource,

groupCollapsing: (group) {

return true; // 返回 true 允许折叠,false 阻止折叠

},

// 其他配置...

)

GroupCollapsed 回调

当分组完全折叠后触发:


SfDataGrid(

source: employeeDataSource,

groupCollapsed: (group) {

// 分组已折叠后的处理逻辑

},

// 其他配置...

)

这些回调函数提供了对分组交互的细粒度控制,使您能够根据业务需求实现自定义逻辑。

自定义分组逻辑

当标准分组技术无法满足特定需求时,SfDataGrid 支持使用自定义逻辑对列进行分组。这可以通过重写 DataGridSource.performGrouping 方法来实现。


@override

String performGrouping(String columnName, DataGridRow row) {

if (columnName == 'Salary') {

final double salary = row

.getCells()

.firstWhereOrNull(

(DataGridCell cell) => cell.columnName == columnName)!

.value;

// 自定义薪资分组逻辑

if (salary > 100000) {

return '高薪资';

} else if (salary > 50000) {

return '中等薪资';

} else {

return '一般薪资';

}

}

return super.performGrouping(columnName, row);

}

这种方法特别适用于需要基于复杂条件或计算值进行分组的场景。

启用分组展开/折叠功能

分组展开和折叠功能可以通过将 SfDataGrid.allowExpandCollapseGroup 属性设置为 true 来启用。此属性的默认值为 false。


SfDataGrid(

source: employeeDataSource,

allowExpandCollapseGroup: true, // 启用展开/折叠功能

// 其他配置...

)

解决分组中的常见问题

获取正确的行数据

在使用分组数据时,开发者可能会遇到一个问题:当尝试使用 onSelectionChanged 或 onCellTapped 事件检索行数据时,返回的是分组数据的第二行而不是第一行。

这个问题的原因是 SfDataGrid 的分组功能为每个分组创建了一个摘要行,而这些摘要行被上述事件视为常规数据行。

解决方案:需要在处理 onSelectionChanged 或 onCellTapped 事件中的行数据时,对分组摘要行进行特殊处理:


onSelectionChanged: (details) {

var selectedRows = details.selectedRows;

if (selectedRows.isNotEmpty) {

var firstSelectedRow = selectedRows[0];

if (firstSelectedRow.groupIndex != null) {

// 选择了分组摘要行,找到对应的数据行索引

int dataRowIndex = firstSelectedRow.rowIndex - firstSelectedRow.groupIndex - 1;

// 现在可以安全地从正确的行检索数据

var rowData = _data[dataRowIndex];

} else {

// 正常情况 - 未涉及分组

var rowData = _data[firstSelectedRow.rowIndex];

}

}

}

这种方法确保无论用户选择的是普通数据行还是分组摘要行,都能获取到正确的数据。

高级功能与技巧

自定义分组图标

可以自定义每个分组级别的展开/折叠图标,通过使用 SfDataGrid.CaptionSummaryTemplate 属性实现:


// 示例:自定义分组图标(基于 .NET MAUI 实现,Flutter 类似)

<syncfusion:SfDataGrid.CaptionSummaryTemplate>

<DataTemplate>

<Grid Padding="5" HorizontalOptions="FillAndExpand">

<Grid.ColumnDefinitions>

<ColumnDefinition Width="Auto"></ColumnDefinition>

<ColumnDefinition Width="*"></ColumnDefinition>

</Grid.ColumnDefinitions>

<Label Grid.Column="1" Text="{Binding Converter={StaticResource SummaryConverter}}">

</Label>

<Image Grid.Column="0" WidthRequest="35"

Source="{Binding Converter={StaticResource SummaryIcon}}"

VerticalOptions="Center" HorizontalOptions="End" HeightRequest="20">

</Image>

</Grid>

</DataTemplate>

</syncfusion:SfDataGrid.CaptionSummaryTemplate>

需要相应的转换器来提供不同的图标资源。

多列汇总显示

SfDataGrid 支持在汇总行中显示多个列的摘要信息:


tableSummaryRows: [

GridTableSummaryRow(

showSummaryInRow: false,

columns: [

const GridSummaryColumn(

name: 'Sum',

columnName: 'salary',

summaryType: GridSummaryType.sum),

const GridSummaryColumn(

name: 'Count',

columnName: 'id',

summaryType: GridSummaryType.count),

const GridSummaryColumn(

name: 'AvgExperience',

columnName: 'experience',

summaryType: GridSummaryType.average),

],

position: GridTableSummaryRowPosition.bottom

)

]

然后通过 buildTableSummaryCellWidget 方法自定义摘要显示:


@override

Widget? buildTableSummaryCellWidget(

GridTableSummaryRow summaryRow,

GridSummaryColumn? summaryColumn,

RowColumnIndex rowColumnIndex,

String summaryValue) {

// 列名到摘要标签的映射

final summaryLabels = {

'id': '计数: $summaryValue',

'experience': '平均: $summaryValue',

'salary': '总和: ${formatter.format(double.tryParse(summaryValue))}',

};

// 获取摘要列的文本

String displayText = summaryLabels[summaryColumn?.columnName] ?? '';

return Center(

child: Container(

padding: const EdgeInsets.all(10.0),

child: Text(displayText),

),

);

}

这种方法允许在每个分组中显示多个列的汇总信息,提供更丰富的数据分析能力。

性能优化建议

当处理大量数据时,考虑以下性能优化建议:

  1. 使用分页:对于大型数据集,实现分页机制以提高性能

  2. 延迟加载:只在需要时加载数据,特别是对于展开的分组

  3. 简化构建逻辑:确保 buildRow 和 buildGroupCaptionCellWidget 方法高效执行

  4. 避免频繁操作:减少对分组列的频繁添加和移除操作

实际应用案例:费用跟踪器

一个很好的数据分组实际应用是费用跟踪器应用。在这种应用中,可以按照以下方式组织数据:

  • 按类型分组:收入和支出

  • 按日期范围分组:每日、每周、每月交易

  • 按类别分组:食品、交通、娱乐等


// 示例:费用跟踪器的多级分组

@override

void initState() {

super.initState();

transactions = getTransactionData();

transactionDataSource = TransactionDataSource(transactionData: transactions);

// 添加多级分组

transactionDataSource.addColumnGroup(

ColumnGroup(name: 'Type', sortGroupRows: true));

transactionDataSource.addColumnGroup(

ColumnGroup(name: 'Category', sortGroupRows: true));

transactionDataSource.addColumnGroup(

ColumnGroup(name: 'Date', sortGroupRows: false));

}

这种分组方式使用户能够快速分析他们的消费模式,识别主要支出类别,并监控收入来源。

总结

Syncfusion Flutter DataGrid 提供了强大而灵活的数据分组功能,可以显著增强数据密集型应用程序的可用性和功能性。通过本文的全面指南,您应该能够:

  1. 理解核心概念:掌握数据分组的基本原理和价值主张

  2. 实现基本分组:通过 ColumnGroup 和 DataGridSource 启用基本分组功能

  3. 自定义分组显示:重写 buildGroupCaptionCellWidget 方法自定义分组标题

  4. 管理分组操作:动态添加、移除和清除分组

  5. 实现多级分组:创建层次化的数据分组结构

  6. 处理分组交互:使用分组回调函数实现自定义交互逻辑

  7. 应用自定义分组逻辑:通过 performGrouping 方法实现高级分组需求

  8. 解决常见问题:正确处理分组行中的数据检索问题

  9. 使用高级功能:实现分组图标自定义和多列汇总显示

原文:xuanhu.info/projects/it…