如何为Flutter交易页面的ListView添加月份日期头部(如[2024年3月])
实现Flutter交易页面按月份分组显示
步骤1:对交易列表按年月分组并排序
在ValueListenableBuilder的builder函数内,先对交易列表按日期倒序排序,再按年份-月份维度分组:
// 按日期倒序排列,确保最新交易优先展示 final sortedTransactions = List.from(newList) ..sort((a, b) => b.date.compareTo(a.date)); // 按"yyyy年MM月"格式分组,将同月份交易归为一组 final groupedTransactions = <String, List<TransactionModel>>{}; for (final transaction in sortedTransactions) { final monthYear = DateFormat('yyyy年MM月').format(transaction.date); groupedTransactions.putIfAbsent(monthYear, () => []); groupedTransactions[monthYear]!.add(transaction); } // 构建混合显示列表:先添加月份头部,再追加对应交易项 final List<dynamic> displayList = []; groupedTransactions.forEach((month, transactions) { displayList.add(month); displayList.addAll(transactions); });
步骤2:修改ListView适配分组结构
调整ListView.separated的配置,根据混合列表的元素类型渲染头部或交易卡片:
return ListView.separated( padding: EdgeInsets.all(10), itemBuilder: (ctx, index) { final item = displayList[index]; // 渲染月份头部 if (item is String) { return Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), color: Colors.grey[100], child: Text( item, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey[700], ), ), ); } // 渲染交易卡片(复用原有Slidable逻辑) else if (item is TransactionModel) { final _list = item; return Slidable( key: Key(_list.id!), startActionPane: ActionPane( motion: DrawerMotion(), children: [ SlidableAction( onPressed: (ctx) { TransactionDB.interface.deleteTransaction(_list.id!); }, icon: Icons.delete, backgroundColor: Colors.red, label: '删除', ), ], ), child: Card( child: ListTile( leading: CircleAvatar( backgroundColor: _list.type == CategoryType.income ? Colors.green : Colors.red, child: Text( parsedDate(_list.date), textAlign: TextAlign.center, ), radius: 30, ), title: Text('Rs ${_list.amount}'), subtitle: Text(_list.categoryModel.name), ), ), ); } return Container(); }, separatorBuilder: (ctx, index) => SizedBox(height: 10), itemCount: displayList.length, );
完整修改后的代码
class ScreenTransaction extends StatelessWidget { const ScreenTransaction({super.key}); @override Widget build(BuildContext context) { TransactionDB.interface.refreshTransaction(); CategoryDB.instance.refreshUI(); return ValueListenableBuilder( valueListenable: TransactionDB.interface.transactionListNotifier, builder: (BuildContext ctx, List<TransactionModel> newList, _) { // 1. 按日期倒序排序 final sortedTransactions = List.from(newList) ..sort((a, b) => b.date.compareTo(a.date)); // 2. 按年月分组 final groupedTransactions = <String, List<TransactionModel>>{}; for (final transaction in sortedTransactions) { final monthYear = DateFormat('yyyy年MM月').format(transaction.date); groupedTransactions.putIfAbsent(monthYear, () => []); groupedTransactions[monthYear]!.add(transaction); } // 3. 构建混合显示列表 final List<dynamic> displayList = []; groupedTransactions.forEach((month, transactions) { displayList.add(month); displayList.addAll(transactions); }); return ListView.separated( padding: EdgeInsets.all(10), itemBuilder: (ctx, index) { final item = displayList[index]; if (item is String) { return Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), color: Colors.grey[100], child: Text( item, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey[700], ), ), ); } else if (item is TransactionModel) { final _list = item; return Slidable( key: Key(_list.id!), startActionPane: ActionPane( motion: DrawerMotion(), children: [ SlidableAction( onPressed: (ctx) { TransactionDB.interface.deleteTransaction(_list.id!); }, icon: Icons.delete, backgroundColor: Colors.red, label: '删除', ), ], ), child: Card( child: ListTile( leading: CircleAvatar( backgroundColor: _list.type == CategoryType.income ? Colors.green : Colors.red, child: Text( parsedDate(_list.date), textAlign: TextAlign.center, ), radius: 30, ), title: Text('Rs ${_list.amount}'), subtitle: Text(_list.categoryModel.name), ), ), ); } return Container(); }, separatorBuilder: (ctx, index) => SizedBox(height: 10), itemCount: displayList.length, ); }, ); } String parsedDate(DateTime date) { final _date = DateFormat.MMMd().format(date); final _splittedDate = _date.split(' '); return '${_splittedDate.last}\n ${_splittedDate.first}'; } }
关键说明
- 分组逻辑:通过
DateFormat('yyyy年MM月')生成统一的月份标识,作为分组的唯一key - 混合列表:将月份头部和对应交易项依次加入列表,保证ListView按「头部+交易」的顺序渲染
- 排序处理:先对交易列表倒序排列,确保最新的月份展示在页面顶部
内容的提问来源于stack exchange,提问作者Midhun S
相关产品推荐
相关产品推荐

