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

Flutter中如何在ListView.builder内添加日期分隔头部?

带月份分隔头部的支出列表实现方案

原生实现(无第三方依赖)

1. 先对支出数据按月份分组

首先把原始的ExpenseModel列表转换成按月份分组的结构,key为格式化后的月份字符串(比如"2024年5月"),value为对应月份的支出项列表:

Map<String, List<ExpenseModel>> groupExpensesByMonth(List<ExpenseModel> expenses) {
  final grouped = <String, List<ExpenseModel>>{};

  for (final expense in expenses) {
    // 按"yyyy年MM月"格式生成分组key
    final monthKey = "${expense.date.year}年${expense.date.month}月";
    grouped.putIfAbsent(monthKey, () => []).add(expense);
  }

  // 对分组按月份倒序排序(保证最新月份在前)
  final sortedKeys = grouped.keys.toList()
    ..sort((a, b) {
      final yearA = int.parse(a.split('年')[0]);
      final monthA = int.parse(a.split('年')[1].replaceAll('月', ''));
      final yearB = int.parse(b.split('年')[0]);
      final monthB = int.parse(b.split('年')[1].replaceAll('月', ''));
      if (yearA != yearB) return yearB.compareTo(yearA);
      return monthB.compareTo(monthA);
    });

  return Map.fromEntries(sortedKeys.map((key) => MapEntry(key, grouped[key]!)));
}

2. 用ListView.builder构建带分隔的列表

计算总条目数(分组数 + 所有支出项数),然后在itemBuilder中判断当前位置是月份头部还是支出项:

// 先拿到排序后的分组key列表
final monthKeys = groupedExpenses.keys.toList();

// 辅助方法:确定当前index属于哪个分组
int getGroupIndex(int index) {
  int cumulativeCount = 0;
  for (int i = 0; i < monthKeys.length; i++) {
    final groupSize = 1 + groupedExpenses[monthKeys[i]]!.length; // 1个头部 + N个条目
    if (index < cumulativeCount + groupSize) {
      return i;
    }
    cumulativeCount += groupSize;
  }
  return -1;
}

// 构建列表
ListView.builder(
  itemCount: monthKeys.length + expenses.length,
  itemBuilder: (context, index) {
    final groupIndex = getGroupIndex(index);
    final currentMonth = monthKeys[groupIndex];
    // 计算当前位置在分组内的偏移量
    final offset = index - (groupIndex > 0 
      ? monthKeys.sublist(0, groupIndex).fold(0, (sum, key) => sum + 1 + groupedExpenses[key]!.length) 
      : 0);

    if (offset == 0) {
      // 渲染月份分隔头部
      return Container(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        color: Colors.grey[200],
        child: Text(
          currentMonth,
          style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
        ),
      );
    } else {
      // 渲染支出项
      final expense = groupedExpenses[currentMonth]![offset - 1];
      return ListTile(
        title: Text(expense.title),
        subtitle: Text("¥${expense.amount.toStringAsFixed(2)}"),
        trailing: Text(expense.date.toString().split(' ')[0]),
      );
    }
  },
)

第三方插件快速实现

如果不想手动处理分组和索引计算,可以用grouped_list插件,开箱即用:

1. 添加依赖

在pubspec.yaml中加入:

dependencies:
  grouped_list: ^5.1.2

2. 直接使用GroupedListView

GroupedListView<ExpenseModel, String>(
  elements: expenses,
  // 按月份分组
  groupBy: (expense) => "${expense.date.year}年${expense.date.month}月",
  // 分组倒序排序
  groupComparator: (a, b) => b.compareTo(a),
  // 组内条目按日期倒序
  itemComparator: (a, b) => b.date.compareTo(a.date),
  // 自定义分隔头部
  groupSeparatorBuilder: (groupValue) => Container(
    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    color: Colors.grey[200],
    child: Text(
      groupValue,
      style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
    ),
  ),
  // 支出项条目构建
  itemBuilder: (context, expense) => ListTile(
    title: Text(expense.title),
    subtitle: Text("¥${expense.amount.toStringAsFixed(2)}"),
    trailing: Text(expense.date.toString().split(' ')[0]),
  ),
)

关键注意点

  • 确保ExpenseModel的date字段是有效的DateTime类型,否则分组逻辑会失效。
  • 排序逻辑可根据需求调整(正序/倒序),保证列表展示顺序符合预期。
  • 原生实现适合需要高度自定义头部或列表交互的场景;插件实现更高效,适合快速开发需求。

内容的提问来源于stack exchange,提问作者Irfan Ganatra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 22:25:41