在这里插入图片描述

记录猫咪每天吃了什么,是科学养猫的基础。今天我们来实现喂食记录列表页面,按日期分组展示,支持滑动删除,让喂食记录一目了然。


功能需求

喂食列表页面需要实现:

  • 按日期分组展示记录
  • 显示食物类型、名称、数量
  • 支持滑动删除
  • 空状态友好提示
  • 快速添加新记录

这种按日期分组的列表在很多App中都很常见。


依赖引入

首先导入需要的包:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../providers/cat_provider.dart';
import '../../models/feeding_record.dart';
import 'add_feeding_screen.dart';

Provider管理喂食数据,数据变化时自动刷新。
intl用于日期格式化和中文星期显示。


无状态组件

喂食列表不需要维护额外状态:

class FeedingListScreen extends StatelessWidget {
  final String catId;

  const FeedingListScreen({super.key, required this.catId});

catId标识是哪只猫咪的喂食记录。
数据由Provider管理,页面本身不需要状态。


页面结构

build方法构建整体布局:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('喂食记录')),
      body: Consumer<CatProvider>(
        builder: (context, provider, child) {
          final records = provider.getFeedingRecordsForCat(catId);

Consumer监听CatProvider的变化。
获取指定猫咪的喂食记录。


空状态处理

没有记录时显示引导界面:

          if (records.isEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.restaurant, size: 80.sp, color: Colors.grey[300]),
                  SizedBox(height: 16.h),
                  Text('暂无喂食记录', style: TextStyle(color: Colors.grey[600])),
                ],
              ),
            );
          }

餐具图标暗示喂食主题。
灰色调表示空状态。


按日期分组

将记录按日期分组:

          final groupedRecords = <String, List<FeedingRecord>>{};
          for (var record in records) {
            final dateKey = DateFormat('yyyy-MM-dd').format(record.dateTime);
            groupedRecords.putIfAbsent(dateKey, () => []).add(record);
          }

用Map存储分组结果,key是日期字符串。
putIfAbsent确保每个日期只创建一次列表。


分组列表展示

用ListView展示分组后的数据:

          return ListView.builder(
            padding: EdgeInsets.all(16.w),
            itemCount: groupedRecords.length,
            itemBuilder: (context, index) {
              final dateKey = groupedRecords.keys.elementAt(index);
              final dayRecords = groupedRecords[dateKey]!;
              return _buildDaySection(context, dateKey, dayRecords, provider);
            },
          );
        },
      ),

itemCount是分组数量,不是记录数量。
每个分组调用_buildDaySection构建。


悬浮添加按钮

页面底部的FAB:

      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.push(context, MaterialPageRoute(
          builder: (_) => AddFeedingScreen(catId: catId),
        )),
        backgroundColor: Colors.orange,
        child: const Icon(Icons.add),
      ),
    );
  }

点击跳转到添加喂食页面。
传入catId标识是哪只猫咪。


日期分组组件

构建一天的记录分组:

  Widget _buildDaySection(BuildContext context, String dateKey, List<FeedingRecord> records, CatProvider provider) {
    final date = DateTime.parse(dateKey);
    final isToday = DateFormat('yyyy-MM-dd').format(DateTime.now()) == dateKey;

解析日期字符串为DateTime对象。
判断是否是今天,用于特殊显示。

日期标题:

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: EdgeInsets.symmetric(vertical: 8.h),
          child: Row(
            children: [
              Text(
                isToday ? '今天' : DateFormat('MM月dd日').format(date),
                style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold, color: Colors.grey[700]),
              ),
              SizedBox(width: 8.w),
              Text(
                DateFormat('EEEE', 'zh_CN').format(date),
                style: TextStyle(fontSize: 12.sp, color: Colors.grey[500]),
              ),
            ],
          ),
        ),

今天显示"今天",其他日期显示月日。
'zh_CN’让星期显示为中文。


当天记录卡片

Card包裹当天的所有记录:

        Card(
          child: ListView.separated(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            itemCount: records.length,
            separatorBuilder: (_, __) => const Divider(height: 1),
            itemBuilder: (context, index) {
              final record = records[index];
              return Dismissible(

shrinkWrap让ListView高度自适应内容。
NeverScrollableScrollPhysics禁止内部滚动。


滑动删除

Dismissible实现滑动删除:

              return Dismissible(
                key: Key(record.id),
                direction: DismissDirection.endToStart,
                background: Container(
                  color: Colors.red,
                  alignment: Alignment.centerRight,
                  padding: EdgeInsets.only(right: 16.w),
                  child: const Icon(Icons.delete, color: Colors.white),
                ),
                onDismissed: (_) => provider.deleteFeedingRecord(record.id),

从右向左滑动显示删除背景。
onDismissed调用Provider删除记录。


记录项内容

ListTile展示记录详情:

                child: ListTile(
                  leading: CircleAvatar(
                    backgroundColor: _getFoodTypeColor(record.foodType).withOpacity(0.1),
                    child: Icon(_getFoodTypeIcon(record.foodType), color: _getFoodTypeColor(record.foodType), size: 20.sp),
                  ),
                  title: Text(record.foodName),
                  subtitle: Text('${record.amount}${record.unit} · ${record.foodTypeString}'),
                  trailing: Text(
                    DateFormat('HH:mm').format(record.dateTime),
                    style: TextStyle(color: Colors.grey[600]),
                  ),
                ),
              );
            },
          ),
        ),
        SizedBox(height: 8.h),
      ],
    );
  }

leading显示食物类型图标。
trailing显示喂食时间。


食物类型颜色

不同类型用不同颜色:

  Color _getFoodTypeColor(FoodType type) {
    switch (type) {
      case FoodType.dryFood: return Colors.brown;
      case FoodType.wetFood: return Colors.orange;
      case FoodType.snack: return Colors.pink;
      case FoodType.water: return Colors.blue;
      case FoodType.other: return Colors.grey;
    }
  }

干粮用棕色,湿粮用橙色,饮水用蓝色。
颜色区分让用户快速识别类型。


食物类型图标

不同类型用不同图标:

  IconData _getFoodTypeIcon(FoodType type) {
    switch (type) {
      case FoodType.dryFood: return Icons.grain;
      case FoodType.wetFood: return Icons.soup_kitchen;
      case FoodType.snack: return Icons.cookie;
      case FoodType.water: return Icons.water_drop;
      case FoodType.other: return Icons.restaurant;
    }
  }
}

图标让类型更直观。
grain表示干粮,soup_kitchen表示湿粮。


分组逻辑详解

Map的putIfAbsent方法:

groupedRecords.putIfAbsent(dateKey, () => []).add(record);

如果key不存在,创建空列表。
然后将记录添加到列表中。

这行代码等价于:

if (!groupedRecords.containsKey(dateKey)) {
  groupedRecords[dateKey] = [];
}
groupedRecords[dateKey]!.add(record);

putIfAbsent更简洁。
一行代码完成判断和添加。


嵌套ListView

ListView嵌套的处理:

ListView.separated(
  shrinkWrap: true,
  physics: const NeverScrollableScrollPhysics(),
  ...
)

shrinkWrap让内部ListView高度自适应。
NeverScrollableScrollPhysics禁止内部滚动,由外部ListView统一滚动。


Divider分隔线

列表项之间的分隔:

separatorBuilder: (_, __) => const Divider(height: 1),

ListView.separated自带分隔线构建器。
height: 1让分隔线很细。


日期格式化

中文星期显示:

DateFormat('EEEE', 'zh_CN').format(date)

EEEE表示完整的星期名称。
'zh_CN’让输出变成中文。

时间格式:

DateFormat('HH:mm').format(record.dateTime)

只显示小时和分钟。
24小时制。


今天判断

判断是否是今天:

final isToday = DateFormat('yyyy-MM-dd').format(DateTime.now()) == dateKey;

将今天的日期格式化后与dateKey比较。
相等则是今天。


Card嵌套布局

Card包裹当天的记录:

Card(
  child: ListView.separated(
    shrinkWrap: true,
    physics: const NeverScrollableScrollPhysics(),
    itemCount: records.length,
    ...
  ),
)

Card提供阴影和圆角效果。
内部的ListView展示当天的所有记录。


小结

喂食列表页面涉及的知识点:

  • 按日期分组的实现
  • 嵌套ListView的处理
  • Dismissible滑动删除
  • 中文日期格式化

这种分组列表的模式在很多场景都能用到。


欢迎加入OpenHarmony跨平台开发社区,一起交流Flutter开发经验:

https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐