Flutter for OpenHarmony 猫咪管家App实战:日历功能开发详解
本文介绍了如何使用Flutter的table_calendar库实现猫咪日程管理日历功能。主要内容包括: 页面状态管理:通过CalendarFormat、_focusedDay和_selectedDay等状态控制日历显示和选择。 日历组件配置:使用TableCalendar设置日期范围、选择回调、事件标记等功能,支持月/周视图切换。 事件集成:从ReminderProvider和HealthPro

日历是管理猫咪日程的好帮手,可以看到哪天有提醒、哪天要打疫苗。今天来实现日历功能,用 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
更多推荐

所有评论(0)