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      # 时间计算

总结

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

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

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

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐