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

相关推荐
夜雨声烦丿2 小时前
Flutter 框架跨平台鸿蒙开发 - 打造功能完整的投票器应用
flutter·华为·harmonyos
酷酷的鱼2 小时前
2026 跨平台开发终极选型:Flutter, React Native 与 uni-app x 深度博标与规划指南
flutter·react native·uni-app
世人万千丶2 小时前
鸿蒙跨端框架 Flutter 学习 Day 4:网络交互——HTTP 请求基础与数据反序列化实战
网络·学习·flutter·ui·交互·harmonyos·鸿蒙
柒儿吖10 小时前
Flutter跨平台三方库animations和flutter_animate在鸿蒙中的使用指南
flutter·华为·harmonyos
安卓理事人14 小时前
鸿蒙list第三个参数的意思
harmonyos
2401_zq136y0316 小时前
Flutter for OpenHarmony:从零搭建今日资讯App(二十五)状态管理的艺术与实践
flutter
IT陈图图16 小时前
基于 Flutter × OpenHarmony 的文本排序工具开发实战
flutter·开源·鸿蒙·openharmony
—Qeyser16 小时前
Flutter 组件通信完全指南
前端·javascript·flutter
奋斗的小青年!!17 小时前
Flutter开发OpenHarmony应用:设置页面组件的深度实践
flutter·harmonyos·鸿蒙