You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用分组数据构建ExpansionTile时遇严重性能问题求优化方案

分组ExpansionTile性能优化方案

核心问题分析

你的代码存在几个关键性能瓶颈:

  • Map的keys.elementAt/values.elementAt是O(n)遍历操作,分组数量越多开销越大
  • 收起状态下仍会创建所有子ListTile,造成不必要的Widget实例化
  • 字符串格式化、正则判断等计算在每次build时重复执行
  • 没有对分组结果做缓存,可能导致重复分组计算

具体优化步骤

1. 预转换分组数据为List,消除O(n)查找

将groupBy返回的Map转换为List条目,让索引访问变为O(1)操作:

// 预转换分组数据,避免每次elementAt的遍历开销
final groupedList = groupedData.entries
    .map((entry) => _GroupEntry(entry.key, entry.value))
    .toList();

// 定义简单的数据类存储分组信息
class _GroupEntry {
  final String groupName;
  final List<ClientsToContactData> clients;

  _GroupEntry(this.groupName, this.clients);
}

// 构建列表时直接通过索引获取分组
return ListView.builder(
  itemCount: groupedList.length,
  itemBuilder: (context, groupIndex) {
    final group = groupedList[groupIndex];
    return ExpansionTile(
      backgroundColor: Colors.blueAccent.withOpacity(0.1),
      title: Text(group.groupName),
      children: [_buildClientList(group.clients)],
    );
  },
);

2. 子列表延迟加载,仅在展开时按需构建

用ListView.builder作为ExpansionTile的子Widget,设置shrinkWrap和禁止独立滚动,确保只有展开时才创建子项:

Widget _buildClientList(List<ClientsToContactData> clients) {
  return ListView.builder(
    shrinkWrap: true,
    physics: const NeverScrollableScrollPhysics(),
    itemCount: clients.length,
    itemBuilder: (context, index) {
      final client = clients[index];
      return ListTile(
        minVerticalPadding: 20,
        title: Text(client.cliente),
        leading: const Icon(Icons.person_rounded),
        trailing: Text(
          client.flagMoraDisplay,
          style: Theme.of(context).textTheme.titleSmall,
        ),
        subtitle: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(client.formattedSaldo),
            Text(client.convenio),
          ],
        ),
      );
    },
  );
}

3. 缓存计算结果,避免重复字符串处理

给数据模型添加扩展方法,缓存格式化后的字符串:

extension ClientsToContactDataExt on ClientsToContactData {
  String get formattedSaldo {
    final raw = saldoVencido.toStringAsFixed(2);
    return '\$ ${raw.replaceAllMapped(
      RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
      (m) => '${m[1]},',
    )}';
  }

  String get flagMoraDisplay {
    return flagMora.contains(RegExp(r'\d')) ? '$flagMora G' : flagMora;
  }
}

4. 缓存分组结果,避免重复计算

将分组逻辑缓存,确保只有数据源变化时才重新分组:

// 用memoized缓存分组结果
final groupedData = _memoizedGroupData(state.clientsToContact!.data!);

Map<String, List<ClientsToContactData>> _memoizedGroupData(List<ClientsToContactData> data) {
  static List<ClientsToContactData>? _lastData;
  static Map<String, List<ClientsToContactData>>? _cachedResult;

  if (_lastData != data) {
    _lastData = data;
    _cachedResult = groupBy(data, (e) => e.flagMora);
  }
  return _cachedResult!;
}

5. 减少Widget重建

  • 对静态Widget使用const构造(如const Icon(Icons.person_rounded))
  • 如果整个列表频繁重建,考虑用ValueListenableBuilder或Consumer仅更新数据源变化的部分
  • 若子项滚动后容易销毁,可给ListTile所在的Widget添加AutomaticKeepAliveClientMixin

内容的提问来源于stack exchange,提问作者Samuel Alcántara

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 07:30:28