请添加图片描述

前言

待办事项的核心价值是"在截止时间前完成"。一个没有截止日期的待办只是"想法",有了截止日期才是"承诺"。Flutter 内置的 showDatePicker 可以直接弹出 Material 3 风格的日期选择器,配合 intl 包的日期格式化,就能构建一套完整的截止日期系统。

鸿蒙 Flutter 备忘录的待办模块允许为每条待办设置截止日期,并在列表中自动标记已逾期的条目——截至日期超过今天,该行文字变为红色加粗提示。

项目仓库:todo_flutter_harmony

依赖

dependencies:
  intl: ^0.20.2

intl 包提供 DateFormat 类,用于日期的格式化和本地化。它是 Dart 官方维护的国际化核心库。

日期选择器的调用

Flutter 内置的 showDatePicker 返回一个 Future<DateTime?>——用户选择日期后返回所选日期,点取消则返回 null:

Future<void> _pickDueDate() async {
  final now = DateTime.now();

  final picked = await showDatePicker(
    context: context,
    initialDate: _dueDate ?? now,
    firstDate: now,                         // 最早可选:今天
    lastDate: DateTime(now.year + 5),       // 最晚可选:5 年后
    builder: (context, child) {
      return Theme(
        data: Theme.of(context).copyWith(
          colorScheme: Theme.of(context).colorScheme.copyWith(
            primary: const Color(0xFF4DB6AC),  // 主题色
          ),
        ),
        child: child!,
      );
    },
  );

  if (!mounted) return;
  if (picked != null) {
    setState(() => _dueDate = picked);
  }
}

三个关键参数:

  • initialDate:初始显示的日期。如果已有截止日期就显示它,否则显示今天
  • firstDate:最早可选日期。设为 DateTime.now() 防止用户选择过去的日期
  • lastDate:最晚可选日期。设为 5 年后,兼顾实用性和合理性

builder 参数用于自定义选择器的主题色——这里将其设为主题薄荷绿 #4DB6AC

DateFormat 格式化

// 在 Todo 卡片中显示截止日期
String _formatDueDate(DateTime dueDate) {
  final now = DateTime.now();
  final tomorrow = DateTime(now.year, now.month, now.day + 1);

  if (_isSameDay(dueDate, now)) {
    return '今天';
  } else if (_isSameDay(dueDate, tomorrow)) {
    return '明天';
  } else if (dueDate.year == now.year) {
    return DateFormat('M月d日').format(dueDate);    // "6月15日"
  } else {
    return DateFormat('yyyy年M月d日').format(dueDate); // "2027年3月1日"
  }
}

bool _isSameDay(DateTime a, DateTime b) {
  return a.year == b.year && a.month == b.month && a.day == b.day;
}

智能格式化:同年的日期省略年份,不同年的才显示年份。“今天”/"明天"的文字比纯数字更友好。

编辑页的日期 UI

class TodoEditPage extends StatefulWidget {
  // ...
}

class _TodoEditPageState extends State<TodoEditPage> {
  DateTime? _dueDate;
  bool _hasDueDate = false;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('待办事项'),
        actions: [
          IconButton(
            icon: const Icon(Icons.check),
            onPressed: _saveTodo,
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 标题输入
            TextField(
              controller: _titleController,
              decoration: const InputDecoration(
                labelText: '标题',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),

            // 备注输入
            TextField(
              controller: _noteController,
              decoration: const InputDecoration(
                labelText: '备注 (可选)',
                border: OutlineInputBorder(),
              ),
              maxLines: 3,
            ),
            const SizedBox(height: 16),

            // 截止日期设置
            SwitchListTile(
              title: const Text('设置截止日期'),
              value: _hasDueDate,
              onChanged: (value) {
                setState(() {
                  _hasDueDate = value;
                  if (!value) {
                    _dueDate = null;
                  } else if (_dueDate == null) {
                    _dueDate = DateTime.now();
                  }
                });
              },
              activeColor: const Color(0xFF4DB6AC),
            ),
            if (_hasDueDate && _dueDate != null)
              ListTile(
                leading: const Icon(Icons.calendar_today),
                title: Text(_formatDueDate(_dueDate!)),
                trailing: const Icon(Icons.edit),
                onTap: _pickDueDate,
              ),
          ],
        ),
      ),
    );
  }
}

逾期的判断

class Todo {
  // ...
  final DateTime? dueDate;

  /// 是否已逾期(仅对未完成且设置了截止日期的待办生效)
  bool get isOverdue {
    if (isCompleted) return false;     // 已完成的不管
    if (dueDate == null) return false; // 没设截止日期的不管
    final now = DateTime.now();
    // 截止日期的当天还没逾期,到第二天才算逾期
    // 即:dueDate 是 5月26日,则 5月26日 23:59 之前都不算逾期
    final deadline = DateTime(dueDate!.year, dueDate!.month, dueDate!.day + 1);
    return now.isAfter(deadline);
  }
}

逾期判断用"截止日期的下一天 00:00"作为比较点,而不是用"今天的日期 > 截止日期"。这样保证截止日期当天都不算逾期——给用户一整个白天的灵活度。

列表中的逾期视觉效果

Widget _buildDueDate(Todo todo) {
  if (todo.dueDate == null) return const SizedBox.shrink();

  return Row(
    children: [
      Icon(
        todo.isOverdue ? Icons.warning_amber_rounded : Icons.calendar_today,
        size: 14,
        color: todo.isOverdue ? Colors.red.shade600 : Colors.grey.shade500,
      ),
      const SizedBox(width: 4),
      Text(
        _formatDueDate(todo.dueDate!),
        style: TextStyle(
          fontSize: 13,
          color: todo.isOverdue ? Colors.red.shade600 : Colors.grey.shade600,
          fontWeight: todo.isOverdue ? FontWeight.w600 : FontWeight.normal,
        ),
      ),
    ],
  );
}

逾期条目的视觉信号:

  1. 红色文字Colors.red.shade600,比标准错误红色略深,避免过于刺眼
  2. 粗体FontWeight.w600,与正常条目形成权重对比
  3. 警告图标Icons.warning_amber_rounded 替代日历图标

这三重变化组合在一起,用户扫一眼列表就知道哪些待办过期了。

排序:逾期优先

TodoProvider 的排序逻辑中,逾期的条目排在前面:

List<Todo> get sortedTodos {
  final sorted = List<Todo>.from(_todos);
  sorted.sort((a, b) {
    // 1. 未完成的在前
    if (a.isCompleted != b.isCompleted) return a.isCompleted ? 1 : -1;
    // 2. 逾期的在前
    if (a.isOverdue != b.isOverdue) return a.isOverdue ? -1 : 1;
    // 3. 有截止日期的在前
    if (a.dueDate != null && b.dueDate == null) return -1;
    if (a.dueDate == null && b.dueDate != null) return 1;
    // 4. 截止日期早的在前
    if (a.dueDate != null && b.dueDate != null) {
      return a.dueDate!.compareTo(b.dueDate!);
    }
    // 5. 创建时间晚的在前
    return b.createdAt.compareTo(a.createdAt);
  });
  return sorted;
}

排序优先级:

  1. 未完成的 > 已完成的
  2. 逾期的 > 未逾期的
  3. 有截止日期 > 无截止日期
  4. 截止日期早 > 截止日期晚
  5. 新创建 > 旧创建

这样的排序把用户最需要关注的条目(已逾期、截止日期将近)放在列表最上方。

鸿蒙兼容性

  • showDatePicker:Material 3 组件,Flutter 框架层实现,不依赖原生日期选择器——它在鸿蒙上与 Android/iOS 的外观完全一致
  • DateFormatintl 包,纯 Dart 实现,与平台无关
  • DateTime 运算:Dart 核心库,与平台无关

showDatePicker 在 Flutter 中是纯 Dart 实现(不是调用原生日期选择器),这意味着在 Android、iOS、鸿蒙 OHOS 上弹出的日期选择器 UI 完全一致。如果你希望各平台使用原生日期选择器风格,可以考虑用 MaterialDatePicker vs CupertinoDatePicker 根据平台切换。

进阶:推送通知提醒

如果未来希望在截止日期到达时推送通知提醒,可以基于现有的 dueDate 字段扩展:

// 在应用启动时检查
void _checkDueSoonTodos(List<Todo> todos) {
  final now = DateTime.now();
  final tomorrow = DateTime(now.year, now.month, now.day + 1);

  for (final todo in todos) {
    if (todo.isCompleted) continue;
    if (todo.dueDate == null) continue;

    if (_isSameDay(todo.dueDate!, tomorrow)) {
      _scheduleReminder(todo);  // 明天截止的,今晚提醒
    }
  }
}

总结

日期选择器和截止日期高亮的实现涉及三个组件:

  1. showDatePicker:弹出 Material 3 日期选择器,限制可选范围(今天 ~ 5 年后)
  2. DateFormat:智能格式化——“今天/明天/6月15日/2027年3月1日”
  3. 逾期判断isOverdue 在截止日第二天 00:00 起算逾期,当天不算

配合排序策略(逾期优先),用户打开待办列表时,最需要关注的条目永远在最上方。

完整项目代码见:todo_flutter_harmony

Logo

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

更多推荐