Flutter 框架跨平台鸿蒙开发 - 打造生日/纪念日倒计时应用

Flutter实战:打造生日/纪念日倒计时应用

前言

倒计时应用能帮助我们记录重要的日子,提醒我们珍惜时间。本文将带你从零开始,使用Flutter开发一个功能完整的倒计时应用,支持多种事件类型、实时倒计时、年度重复等功能。

应用特色

  • 实时倒计时:精确到秒的实时更新
  • 🎂 七种事件类型:生日、纪念日、节日、考试、旅行、会议、其他
  • 🔄 年度重复:生日等事件自动计算下一年
  • 📊 统计分析:事件数量、类型分布统计
  • 🎨 渐变设计:每种类型独特配色
  • 📅 日期选择:直观的日期选择器
  • 💾 数据持久化:本地存储所有事件
  • 🗂️ 分类显示:进行中和已过期分开显示
  • 🌓 深色模式:自动适配系统主题

效果展示




倒计时
事件类型
生日
纪念日
节日
考试
旅行
会议
其他
核心功能
实时倒计时
天数
小时
分钟
秒数
年度重复
自动计算下一年
适用生日纪念日
事件管理
新建编辑
删除确认
分类显示
统计分析
总数统计
类型分布
状态统计

数据模型设计

1. 事件类型枚举

dart 复制代码
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. 倒计时事件模型

dart 复制代码
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每秒更新一次:

dart 复制代码
class _CountdownPageState extends State<CountdownPage> {
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _loadEvents();
    // 每秒更新一次
    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
      setState(() {});
    });
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }
}

2. 年度重复计算

自动计算下一年的目标日期:

dart 复制代码
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. 时间差计算

计算剩余的天、时、分、秒:

dart 复制代码
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. 事件排序

按剩余天数排序:

dart 复制代码
void _sortEvents() {
  _events.sort((a, b) {
    final aDays = a.daysRemaining;
    final bDays = b.daysRemaining;
    return aDays.compareTo(bDays);
  });
}

5. 倒计时显示组件

dart 复制代码
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. 事件卡片

dart 复制代码
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. 分类显示

dart 复制代码
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. 编辑面板

dart 复制代码
class EventEditSheet extends StatefulWidget {
  final CountdownEvent? event;
  final Function(CountdownEvent) onSave;

  @override
  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. 统计对话框

dart 复制代码
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() 取消定时器
dart 复制代码
// 每秒执行一次
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
  setState(() {});
});

// 延迟3秒执行
Timer(const Duration(seconds: 3), () {
  print('3秒后执行');
});

// 取消定时器
_timer?.cancel();

2. DateTime时间计算

dart 复制代码
// 获取当前时间
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:

dart 复制代码
CustomScrollView(
  slivers: [
    SliverAppBar.large(title: Text('标题')),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => _buildItem(index),
        childCount: items.length,
      ),
    ),
  ],
)

4. ChoiceChip选择器

dart 复制代码
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. 桌面小部件

显示最近的倒计时:

dart 复制代码
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. 通知提醒

提前提醒重要日期:

dart 复制代码
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. 背景图片

为不同类型设置背景:

dart 复制代码
class EventCard extends StatelessWidget {
  final CountdownEvent event;

  @override
  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. 分享功能

分享倒计时卡片:

dart 复制代码
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. 日历视图

以日历形式显示事件:

dart 复制代码
import 'package:table_calendar/table_calendar.dart';

class CalendarView extends StatelessWidget {
  final List<CountdownEvent> events;

  @override
  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. 进度条显示

显示时间进度:

dart 复制代码
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);
  }

  @override
  Widget build(BuildContext context) {
    return LinearProgressIndicator(
      value: progress,
      backgroundColor: Colors.grey.shade200,
      valueColor: AlwaysStoppedAnimation(event.type.color),
    );
  }
}

性能优化建议

1. 定时器优化

只在可见时更新:

dart 复制代码
class _CountdownPageState extends State<CountdownPage>
    with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _startTimer();
  }

  @override
  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保持状态:

dart 复制代码
class EventCard extends StatefulWidget {
  @override
  State<EventCard> createState() => _EventCardState();
}

class _EventCardState extends State<EventCard>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Card(...);
  }
}

3. 数据缓存

缓存计算结果:

dart 复制代码
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会自动处理:

dart 复制代码
// 如果今年不是闰年,会自动调整为2月28日
final date = DateTime(2023, 2, 29);  // 实际是2023-03-01

Q2: 如何实现倒数日(已过多少天)?

A: 修改计算逻辑:

dart 复制代码
int get daysPassed {
  final now = DateTime.now();
  final difference = now.difference(targetDate);
  return difference.inDays;
}

Q3: 如何添加时区支持?

A: 使用timezone包:

dart 复制代码
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      # 时间计算

总结

本文实现了一个功能完整的倒计时应用,涵盖了以下核心技术:

  1. Timer定时器:实时更新倒计时
  2. DateTime计算:时间差、日期比较
  3. 年度重复:自动计算下一年日期
  4. SliverList:高性能列表
  5. 数据持久化:SharedPreferences存储

通过本项目,你不仅学会了如何实现倒计时应用,还掌握了Flutter中时间处理、定时任务、性能优化的核心技术。这些知识可以应用到更多场景,如番茄钟、计时器、日程管理等领域。

时间宝贵,珍惜每一天。希望这个应用能帮助你更好地规划和记录重要的日子!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
小哥Mark1 小时前
Flutter开发鸿蒙年味 + 实用实战应用|绿色烟花:电子烟花 + 手持烟花
flutter·华为·harmonyos
小镇敲码人1 小时前
剖析CANN框架中Samples仓库:从示例到实战的AI开发指南
c++·人工智能·python·华为·acl·cann
前端不太难2 小时前
HarmonyOS 游戏里,Ability 是如何被重建的
游戏·状态模式·harmonyos
lbb 小魔仙2 小时前
【HarmonyOS实战】React Native 鸿蒙版实战:Calendar 日历组件完全指南
react native·react.js·harmonyos
一只大侠的侠2 小时前
Flutter开源鸿蒙跨平台训练营 Day 3
flutter·开源·harmonyos
盐焗西兰花3 小时前
鸿蒙学习实战之路-Reader Kit自定义字体最佳实践
学习·华为·harmonyos
_waylau3 小时前
鸿蒙架构师修炼之道-架构师的职责是什么?
开发语言·华为·harmonyos·鸿蒙
一只大侠的侠4 小时前
【Harmonyos】Flutter开源鸿蒙跨平台训练营 Day 2 鸿蒙跨平台开发环境搭建与工程实践
flutter·开源·harmonyos
微祎_5 小时前
Flutter for OpenHarmony:构建一个 Flutter 平衡球游戏,深入解析动画控制器、实时物理模拟与手势驱动交互
flutter·游戏·交互
ZH15455891316 小时前
Flutter for OpenHarmony Python学习助手实战:面向对象编程实战的实现
python·学习·flutter