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
相关产品推荐
相关产品推荐

