在这里插入图片描述

日历是管理猫咪日程的好帮手,可以看到哪天有提醒、哪天要打疫苗。今天来实现日历功能,用 table_calendar 库展示日历,点击日期显示当天的事件列表。

一、页面状态管理

日历页面需要管理选中日期等状态:

class CalendarScreen extends StatefulWidget {
  const CalendarScreen({super.key});

  
  State<CalendarScreen> createState() => _CalendarScreenState();
}

class _CalendarScreenState extends State<CalendarScreen> {
  CalendarFormat _calendarFormat = CalendarFormat.month;
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;

_calendarFormat 控制日历显示格式,月视图或周视图。
_focusedDay 是当前聚焦的日期,_selectedDay 是选中的日期。

二、页面整体结构

用 Consumer2 监听两个 Provider:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('日历')),
    body: Consumer2<ReminderProvider, HealthProvider>(
      builder: (context, reminderProvider, healthProvider, child) {
        return Column(
          children: [
            _buildCalendar(reminderProvider, healthProvider),
            const Divider(height: 1),
            Expanded(
              child: _buildEventList(reminderProvider, healthProvider),
            ),
          ],
        );
      },
    ),
  );
}

Consumer2 可以同时监听两个 Provider。
提醒和健康记录都可能有日程事件。

布局结构:

上面是日历组件,下面是事件列表。
Divider 分隔两个区域。

三、日历组件

TableCalendar 配置:

Widget _buildCalendar(ReminderProvider reminderProvider, HealthProvider healthProvider) {
  return TableCalendar(
    firstDay: DateTime(2020),
    lastDay: DateTime(2100),
    focusedDay: _focusedDay,
    calendarFormat: _calendarFormat,
    selectedDayPredicate: (day) => isSameDay(_selectedDay, day),

firstDay 和 lastDay 定义日历范围。
selectedDayPredicate 判断某天是否被选中。

日期选择回调:

onDaySelected: (selectedDay, focusedDay) {
  setState(() {
    _selectedDay = selectedDay;
    _focusedDay = focusedDay;
  });
},
onFormatChanged: (format) {
  setState(() => _calendarFormat = format);
},
onPageChanged: (focusedDay) {
  _focusedDay = focusedDay;
},

onDaySelected 在点击日期时触发。
onFormatChanged 在切换月/周视图时触发。

四、事件标记

eventLoader 加载事件:

eventLoader: (day) {
  final events = <dynamic>[];
  // Add reminders
  for (var reminder in reminderProvider.reminders) {
    if (isSameDay(reminder.dateTime, day)) {
      events.add(reminder);
    }
  }
  // Add health records with next date
  for (var record in healthProvider.healthRecords) {
    if (record.nextDate != null && isSameDay(record.nextDate!, day)) {
      events.add(record);
    }
  }
  return events;
},

遍历提醒和健康记录,找出当天的事件。
健康记录的 nextDate 是下次提醒日期。

isSameDay 的作用:

比较两个日期是否是同一天。
忽略时分秒,只比较年月日。

五、日历样式

样式配置:

calendarStyle: CalendarStyle(
  todayDecoration: BoxDecoration(
    color: Colors.orange.withOpacity(0.5),
    shape: BoxShape.circle,
  ),
  selectedDecoration: const BoxDecoration(
    color: Colors.orange,
    shape: BoxShape.circle,
  ),
  markerDecoration: const BoxDecoration(
    color: Colors.blue,
    shape: BoxShape.circle,
  ),
),

todayDecoration 是今天的样式,半透明橙色。
selectedDecoration 是选中日期的样式,实心橙色。

markerDecoration:

有事件的日期下面会显示小圆点。
蓝色圆点表示当天有事件。

头部样式:

headerStyle: const HeaderStyle(
  formatButtonVisible: true,
  titleCentered: true,
),

formatButtonVisible 显示切换月/周的按钮。
titleCentered 让标题居中。

六、事件列表

获取选中日期的事件:

Widget _buildEventList(ReminderProvider reminderProvider, HealthProvider healthProvider) {
  final selectedDay = _selectedDay ?? _focusedDay;
  final events = <dynamic>[];

  // Get reminders for selected day
  for (var reminder in reminderProvider.reminders) {
    if (isSameDay(reminder.dateTime, selectedDay)) {
      events.add(reminder);
    }
  }

  // Get health records with next date
  for (var record in healthProvider.healthRecords) {
    if (record.nextDate != null && isSameDay(record.nextDate!, selectedDay)) {
      events.add(record);
    }
  }

如果没有选中日期,用聚焦日期。
收集当天的所有事件。

空状态:

if (events.isEmpty) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.event_available, size: 60.sp, color: Colors.grey[300]),
        SizedBox(height: 16.h),
        Text(
          '${DateFormat('MM月dd日').format(selectedDay)} 暂无事件',
          style: TextStyle(color: Colors.grey[600]),
        ),
      ],
    ),
  );
}

显示日期和"暂无事件"提示。
用户知道选中的是哪天。

七、事件卡片

根据类型渲染:

return ListView.builder(
  padding: EdgeInsets.all(16.w),
  itemCount: events.length,
  itemBuilder: (context, index) {
    final event = events[index];
    if (event is ReminderModel) {
      return _buildReminderCard(event);
    } else if (event is HealthRecord) {
      return _buildHealthCard(event);
    }
    return const SizedBox();
  },
);

用 is 判断事件类型。
不同类型用不同的卡片样式。

提醒卡片:

Widget _buildReminderCard(ReminderModel reminder) {
  return Card(
    margin: EdgeInsets.only(bottom: 8.h),
    child: ListTile(
      leading: CircleAvatar(
        backgroundColor: Colors.orange[100],
        child: Icon(Icons.alarm, color: Colors.orange, size: 20.sp),
      ),
      title: Text(reminder.title),
      subtitle: Text('${reminder.typeString} · ${DateFormat('HH:mm').format(reminder.dateTime)}'),
      trailing: Container(
        padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
        decoration: BoxDecoration(
          color: Colors.orange[100],
          borderRadius: BorderRadius.circular(12.r),
        ),
        child: const Text('提醒', style: TextStyle(color: Colors.orange)),
      ),
    ),
  );
}

橙色图标和标签表示这是提醒。
显示提醒时间,精确到分钟。

健康卡片:

Widget _buildHealthCard(HealthRecord record) {
  return Card(
    margin: EdgeInsets.only(bottom: 8.h),
    child: ListTile(
      leading: CircleAvatar(
        backgroundColor: Colors.blue[100],
        child: Icon(Icons.medical_services, color: Colors.blue, size: 20.sp),
      ),
      title: Text(record.title),
      subtitle: Text('${record.typeString} 到期'),
      trailing: Container(
        padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
        decoration: BoxDecoration(
          color: Colors.blue[100],
          borderRadius: BorderRadius.circular(12.r),
        ),
        child: const Text('健康', style: TextStyle(color: Colors.blue)),
      ),
    ),
  );
}

蓝色图标和标签表示这是健康记录。
显示记录类型和"到期"提示。

八、Consumer2 的使用

监听多个 Provider:

Consumer2<ReminderProvider, HealthProvider>(
  builder: (context, reminderProvider, healthProvider, child) {
    // 可以访问两个 provider
  },
)

Consumer2 的泛型参数是两个 Provider 类型。
builder 回调有两个 provider 参数。

还有 Consumer3、Consumer4 等:

最多支持 Consumer6。
如果需要更多,可以嵌套使用。

九、日期比较

isSameDay 函数:

if (isSameDay(reminder.dateTime, day)) {
  events.add(reminder);
}

isSameDay 来自 table_calendar 包。
只比较年月日,忽略时分秒。

为什么不用 == :

// 这样比较会失败
reminder.dateTime == day

DateTime 的 == 比较包括时分秒。
同一天不同时间的两个 DateTime 不相等。

十、动态类型列表

List 的使用:

final events = <dynamic>[];
events.add(reminder);  // ReminderModel
events.add(record);    // HealthRecord

不同类型的事件放在同一个列表里。
渲染时用 is 判断类型。

类型安全的替代方案:

// 定义一个基类或接口
abstract class CalendarEvent {}
class ReminderEvent extends CalendarEvent {}
class HealthEvent extends CalendarEvent {}

用基类可以获得类型检查。
但对于简单场景,dynamic 够用了。

十一、日期格式化

显示日期:

DateFormat('MM月dd日').format(selectedDay)

中文环境用"月日"更自然。
比如"01月15日"。

显示时间:

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

HH 是 24 小时制。
比如"14:30"。

十二、标签样式

圆角标签:

Container(
  padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
  decoration: BoxDecoration(
    color: Colors.orange[100],
    borderRadius: BorderRadius.circular(12.r),
  ),
  child: const Text('提醒', style: TextStyle(color: Colors.orange)),
),

浅色背景配深色文字。
圆角让标签更柔和。

为什么用标签:

快速区分事件类型。
用户一眼就知道是提醒还是健康记录。

小结

日历功能用 table_calendar 实现日历展示,支持月视图和周视图切换。有事件的日期显示小圆点标记,点击日期显示当天的事件列表。代码上用 Consumer2 监听两个 Provider,用 eventLoader 加载事件标记,用 is 判断事件类型渲染不同样式的卡片。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐