Flutter 框架跨平台鸿蒙开发 - 打造生日/纪念日倒计时应用
Timer定时器:实时更新倒计时DateTime计算:时间差、日期比较年度重复:自动计算下一年日期SliverList:高性能列表数据持久化:SharedPreferences存储通过本项目,你不仅学会了如何实现倒计时应用,还掌握了Flutter中时间处理、定时任务、性能优化的核心技术。这些知识可以应用到更多场景,如番茄钟、计时器、日程管理等领域。时间宝贵,珍惜每一天。希望这个应用能帮助你更好地规
·
Flutter实战:打造生日/纪念日倒计时应用
前言
倒计时应用能帮助我们记录重要的日子,提醒我们珍惜时间。本文将带你从零开始,使用Flutter开发一个功能完整的倒计时应用,支持多种事件类型、实时倒计时、年度重复等功能。
应用特色
- ⏰ 实时倒计时:精确到秒的实时更新
- 🎂 七种事件类型:生日、纪念日、节日、考试、旅行、会议、其他
- 🔄 年度重复:生日等事件自动计算下一年
- 📊 统计分析:事件数量、类型分布统计
- 🎨 渐变设计:每种类型独特配色
- 📅 日期选择:直观的日期选择器
- 💾 数据持久化:本地存储所有事件
- 🗂️ 分类显示:进行中和已过期分开显示
- 🌓 深色模式:自动适配系统主题
效果展示



数据模型设计
1. 事件类型枚举
enum EventType {
birthday, // 生日
anniversary, // 纪念日
holiday, // 节日
exam, // 考试
travel, // 旅行
meeting, // 会议
other; // 其他
String get label {
switch (this) {
case EventType.birthday: return '生日';
case EventType.anniversary: return '纪念日';
// ...
}
}
IconData get icon {
switch (this) {
case EventType.birthday: return Icons.cake;
case EventType.anniversary: return Icons.favorite;
// ...
}
}
Color get color {
switch (this) {
case EventType.birthday: return Colors.pink;
case EventType.anniversary: return Colors.red;
// ...
}
}
}
2. 倒计时事件模型
class CountdownEvent {
String id;
String title;
DateTime targetDate;
EventType type;
String? description;
bool isYearly; // 是否每年重复
CountdownEvent({
required this.id,
required this.title,
required this.targetDate,
required this.type,
this.description,
this.isYearly = false,
});
// 获取下一个目标日期(用于年度重复事件)
DateTime get nextTargetDate {
if (!isYearly) return targetDate;
final now = DateTime.now();
var nextDate = DateTime(
now.year,
targetDate.month,
targetDate.day,
);
if (nextDate.isBefore(now)) {
nextDate = DateTime(
now.year + 1,
targetDate.month,
targetDate.day,
);
}
return nextDate;
}
// 计算剩余天数
int get daysRemaining {
final now = DateTime.now();
final target = isYearly ? nextTargetDate : targetDate;
final difference = target.difference(now);
return difference.inDays;
}
// 计算剩余时间(天、小时、分钟、秒)
Map<String, int> get timeRemaining {
final now = DateTime.now();
final target = isYearly ? nextTargetDate : targetDate;
final difference = target.difference(now);
return {
'days': difference.inDays,
'hours': difference.inHours % 24,
'minutes': difference.inMinutes % 60,
'seconds': difference.inSeconds % 60,
};
}
// 是否已过期
bool get isExpired {
if (isYearly) return false;
return targetDate.isBefore(DateTime.now());
}
}
核心功能实现
1. 实时倒计时更新
使用Timer每秒更新一次:
class _CountdownPageState extends State<CountdownPage> {
Timer? _timer;
void initState() {
super.initState();
_loadEvents();
// 每秒更新一次
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
setState(() {});
});
}
void dispose() {
_timer?.cancel();
super.dispose();
}
}
2. 年度重复计算
自动计算下一年的目标日期:
DateTime get nextTargetDate {
if (!isYearly) return targetDate;
final now = DateTime.now();
// 使用今年的月日
var nextDate = DateTime(
now.year,
targetDate.month,
targetDate.day,
);
// 如果今年的日期已过,使用明年
if (nextDate.isBefore(now)) {
nextDate = DateTime(
now.year + 1,
targetDate.month,
targetDate.day,
);
}
return nextDate;
}
3. 时间差计算
计算剩余的天、时、分、秒:
Map<String, int> get timeRemaining {
final now = DateTime.now();
final target = isYearly ? nextTargetDate : targetDate;
final difference = target.difference(now);
return {
'days': difference.inDays,
'hours': difference.inHours % 24,
'minutes': difference.inMinutes % 60,
'seconds': difference.inSeconds % 60,
};
}
4. 事件排序
按剩余天数排序:
void _sortEvents() {
_events.sort((a, b) {
final aDays = a.daysRemaining;
final bDays = b.daysRemaining;
return aDays.compareTo(bDays);
});
}
5. 倒计时显示组件
Widget _buildTimeUnit(int value, String unit, Color color) {
return Column(
children: [
Text(
value.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
unit,
style: TextStyle(
fontSize: 12,
color: color.withOpacity(0.7),
),
),
],
);
}
// 使用
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildTimeUnit(timeRemaining['days']!, '天', color),
_buildTimeUnit(timeRemaining['hours']!, '时', color),
_buildTimeUnit(timeRemaining['minutes']!, '分', color),
_buildTimeUnit(timeRemaining['seconds']!, '秒', color),
],
)
UI组件设计
1. 事件卡片
Widget _buildEventCard(CountdownEvent event) {
final isExpired = event.isExpired;
final timeRemaining = event.timeRemaining;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头部:图标、标题、类型、删除按钮
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: event.type.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
event.type.icon,
color: event.type.color,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
// 类型标签
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: event.type.color.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(event.type.label),
),
// 年度重复标签
if (event.isYearly)
Container(
child: const Text('每年'),
),
],
),
],
),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _deleteEvent(event),
),
],
),
const SizedBox(height: 16),
// 倒计时显示
if (isExpired)
Container(
child: const Text('已过期'),
)
else
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
event.type.color.withOpacity(0.1),
event.type.color.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildTimeUnit(timeRemaining['days']!, '天', color),
_buildTimeUnit(timeRemaining['hours']!, '时', color),
_buildTimeUnit(timeRemaining['minutes']!, '分', color),
_buildTimeUnit(timeRemaining['seconds']!, '秒', color),
],
),
),
],
),
),
);
}
2. 分类显示
final activeEvents = _events.where((e) => !e.isExpired).toList();
final expiredEvents = _events.where((e) => e.isExpired).toList();
// 进行中的事件
if (activeEvents.isNotEmpty) ...[
SliverToBoxAdapter(
child: Text('进行中 (${activeEvents.length})'),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildEventCard(activeEvents[index]),
childCount: activeEvents.length,
),
),
],
// 已过期的事件
if (expiredEvents.isNotEmpty) ...[
SliverToBoxAdapter(
child: Text('已过期 (${expiredEvents.length})'),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildEventCard(expiredEvents[index]),
childCount: expiredEvents.length,
),
),
],
3. 编辑面板
class EventEditSheet extends StatefulWidget {
final CountdownEvent? event;
final Function(CountdownEvent) onSave;
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: Column(
children: [
// 标题输入
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题',
prefixIcon: Icon(Icons.title),
),
),
// 类型选择
Wrap(
children: EventType.values.map((type) {
return ChoiceChip(
avatar: Icon(type.icon),
label: Text(type.label),
selected: _type == type,
onSelected: (selected) {
setState(() => _type = type);
},
);
}).toList(),
),
// 日期选择
ListTile(
title: const Text('目标日期'),
subtitle: Text(_formatDate(_targetDate)),
onTap: _selectDate,
),
// 年度重复开关
SwitchListTile(
title: const Text('每年重复'),
value: _isYearly,
onChanged: (value) {
setState(() => _isYearly = value);
},
),
],
),
),
);
}
}
4. 统计对话框
void _showStatistics() {
final total = _events.length;
final active = _events.where((e) => !e.isExpired).length;
final expired = _events.where((e) => e.isExpired).length;
final yearly = _events.where((e) => e.isYearly).length;
// 统计各类型数量
final typeCount = <EventType, int>{};
for (var event in _events) {
typeCount[event.type] = (typeCount[event.type] ?? 0) + 1;
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('📊 统计信息'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildStatRow('总事件数', total.toString()),
_buildStatRow('进行中', active.toString()),
_buildStatRow('已过期', expired.toString()),
_buildStatRow('年度重复', yearly.toString()),
const Divider(),
// 类型分布
...typeCount.entries.map((entry) {
return Row(
children: [
Icon(entry.key.icon, color: entry.key.color),
Text(entry.key.label),
const Spacer(),
Text(entry.value.toString()),
],
);
}),
],
),
),
);
}
技术要点详解
1. Timer定时器
| 方法 | 说明 |
|---|---|
| Timer.periodic | 周期性执行 |
| Timer | 延迟执行一次 |
| cancel() | 取消定时器 |
// 每秒执行一次
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {});
});
// 延迟3秒执行
Timer(const Duration(seconds: 3), () {
print('3秒后执行');
});
// 取消定时器
_timer?.cancel();
2. DateTime时间计算
// 获取当前时间
final now = DateTime.now();
// 创建指定日期
final date = DateTime(2024, 12, 31);
// 时间差
final difference = date.difference(now);
// 获取各单位
final days = difference.inDays;
final hours = difference.inHours % 24;
final minutes = difference.inMinutes % 60;
final seconds = difference.inSeconds % 60;
// 日期比较
if (date.isBefore(now)) {
print('已过期');
}
3. SliverList性能优化
使用SliverList而不是ListView:
CustomScrollView(
slivers: [
SliverAppBar.large(title: Text('标题')),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildItem(index),
childCount: items.length,
),
),
],
)
4. ChoiceChip选择器
Wrap(
spacing: 8,
children: options.map((option) {
return ChoiceChip(
avatar: Icon(option.icon),
label: Text(option.label),
selected: _selected == option,
onSelected: (selected) {
setState(() => _selected = option);
},
);
}).toList(),
)
功能扩展建议
1. 桌面小部件
显示最近的倒计时:
import 'package:home_widget/home_widget.dart';
class WidgetService {
static Future<void> updateWidget() async {
final event = await getNextEvent();
await HomeWidget.saveWidgetData<String>('title', event.title);
await HomeWidget.saveWidgetData<int>('days', event.daysRemaining);
await HomeWidget.updateWidget(
name: 'CountdownWidget',
iOSName: 'CountdownWidget',
);
}
}
2. 通知提醒
提前提醒重要日期:
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class NotificationService {
static Future<void> scheduleReminder(CountdownEvent event) async {
final notifications = FlutterLocalNotificationsPlugin();
// 提前1天提醒
final reminderDate = event.targetDate.subtract(
const Duration(days: 1),
);
await notifications.zonedSchedule(
event.id.hashCode,
'明天是${event.title}',
'还有1天',
tz.TZDateTime.from(reminderDate, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails(
'countdown_reminder',
'倒计时提醒',
),
),
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
}
}
3. 背景图片
为不同类型设置背景:
class EventCard extends StatelessWidget {
final CountdownEvent event;
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/bg_${event.type.name}.jpg'),
fit: BoxFit.cover,
opacity: 0.3,
),
),
child: _buildContent(),
);
}
}
4. 分享功能
分享倒计时卡片:
import 'package:share_plus/share_plus.dart';
Future<void> shareEvent(CountdownEvent event) async {
final text = '''
${event.title}
还有 ${event.daysRemaining} 天
${_formatDate(event.targetDate)}
''';
await Share.share(text);
}
5. 日历视图
以日历形式显示事件:
import 'package:table_calendar/table_calendar.dart';
class CalendarView extends StatelessWidget {
final List<CountdownEvent> events;
Widget build(BuildContext context) {
return TableCalendar(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: DateTime.now(),
eventLoader: (day) {
return events.where((event) {
final target = event.isYearly
? event.nextTargetDate
: event.targetDate;
return isSameDay(target, day);
}).toList();
},
);
}
}
6. 进度条显示
显示时间进度:
class ProgressIndicator extends StatelessWidget {
final CountdownEvent event;
double get progress {
final total = event.targetDate.difference(event.createdAt).inDays;
final remaining = event.daysRemaining;
return 1 - (remaining / total);
}
Widget build(BuildContext context) {
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation(event.type.color),
);
}
}
性能优化建议
1. 定时器优化
只在可见时更新:
class _CountdownPageState extends State<CountdownPage>
with WidgetsBindingObserver {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_startTimer();
}
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_startTimer();
} else {
_timer?.cancel();
}
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) setState(() {});
});
}
}
2. 列表优化
使用AutomaticKeepAliveClientMixin保持状态:
class EventCard extends StatefulWidget {
State<EventCard> createState() => _EventCardState();
}
class _EventCardState extends State<EventCard>
with AutomaticKeepAliveClientMixin {
bool get wantKeepAlive => true;
Widget build(BuildContext context) {
super.build(context);
return Card(...);
}
}
3. 数据缓存
缓存计算结果:
class CountdownEvent {
Map<String, int>? _cachedTimeRemaining;
DateTime? _lastCalculated;
Map<String, int> get timeRemaining {
final now = DateTime.now();
// 如果缓存有效(1秒内),直接返回
if (_cachedTimeRemaining != null &&
_lastCalculated != null &&
now.difference(_lastCalculated!).inSeconds < 1) {
return _cachedTimeRemaining!;
}
// 重新计算
_cachedTimeRemaining = _calculateTimeRemaining();
_lastCalculated = now;
return _cachedTimeRemaining!;
}
}
常见问题解答
Q1: 如何处理闰年2月29日?
A: DateTime会自动处理:
// 如果今年不是闰年,会自动调整为2月28日
final date = DateTime(2023, 2, 29); // 实际是2023-03-01
Q2: 如何实现倒数日(已过多少天)?
A: 修改计算逻辑:
int get daysPassed {
final now = DateTime.now();
final difference = now.difference(targetDate);
return difference.inDays;
}
Q3: 如何添加时区支持?
A: 使用timezone包:
import 'package:timezone/timezone.dart' as tz;
DateTime get localTargetDate {
final location = tz.getLocation('Asia/Shanghai');
return tz.TZDateTime.from(targetDate, location);
}
项目结构
lib/
├── main.dart # 主程序入口
├── models/
│ ├── event_type.dart # 事件类型枚举
│ └── countdown_event.dart # 倒计时事件模型
├── screens/
│ ├── countdown_page.dart # 主页面
│ └── event_edit_sheet.dart # 编辑面板
├── widgets/
│ ├── event_card.dart # 事件卡片
│ ├── time_unit.dart # 时间单位组件
│ └── statistics_dialog.dart # 统计对话框
├── services/
│ ├── storage_service.dart # 存储服务
│ └── notification_service.dart # 通知服务
└── utils/
├── date_formatter.dart # 日期格式化
└── time_calculator.dart # 时间计算
总结
本文实现了一个功能完整的倒计时应用,涵盖了以下核心技术:
- Timer定时器:实时更新倒计时
- DateTime计算:时间差、日期比较
- 年度重复:自动计算下一年日期
- SliverList:高性能列表
- 数据持久化:SharedPreferences存储
通过本项目,你不仅学会了如何实现倒计时应用,还掌握了Flutter中时间处理、定时任务、性能优化的核心技术。这些知识可以应用到更多场景,如番茄钟、计时器、日程管理等领域。
时间宝贵,珍惜每一天。希望这个应用能帮助你更好地规划和记录重要的日子!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)