Flutter for OpenHarmony:票匣系统 - 基于Flutter的会话级票据管理实践与用户体验设计

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

发布时间:2026年2月9日

技术栈:Flutter 3.22+、Dart 3.4+、状态管理、FilterChip、AlertDialog、枚举驱动 UI、日期解析
项目类型:效率工具 / 个人资产管理 / 教育级 CRUD 应用
适用读者:Flutter 开发者、产品设计师、对“轻量级数据管理”有需求的用户


引言:在数字洪流中,为实体票据留一席之地

尽管无纸化浪潮席卷全球,我们仍频繁接触各类数字票据:电影票、地铁卡、演唱会入场码、优惠券……它们散落在短信、邮件、App 推送中,有效期各异,极易遗忘或过期。而传统笔记应用又过于笨重,无法提供结构化管理。

《票匣》(TicketHub)应运而生:一个极简、会话内运行、零持久化依赖的票据管理器。它不请求存储权限,不连接云端,仅在当前页面生命周期内保存数据——关闭即清空,却足以应对“今天看什么电影”“周卡还剩几天”等高频临时查询场景。

本文将深入剖析其五大核心维度:

  1. 枚举驱动的类型系统与筛选架构
  2. 表单验证与用户输入容错设计
  3. 有效期智能着色与状态感知 UI
  4. FilterChip 实现的语义化筛选体验
  5. 诚实告知数据生命周期:克制式产品哲学

并探讨如何在不引入任何状态管理库的前提下,构建清晰、可维护的 CRUD 应用。
在这里插入图片描述


一、领域建模:用 Dart 枚举定义业务语义

1.1 强类型票据分类

enum TicketType {
  all('全部'),
  movie('电影'),
  transport('交通'),
  event('活动'),
  coupon('优惠');

  final String label;
  const TicketType(this.label);
}

在这里插入图片描述

设计优势:
  • 语义明确TicketType.movie 比字符串 "movie" 更安全
  • 本地化友好label 字段支持未来多语言扩展
  • 编译时检查:避免拼写错误导致的运行时 bug

1.2 票据数据结构

class TicketItem {
  final String id;
  final String name;
  final TicketType type;
  final DateTime expiryDate;
}

在这里插入图片描述

  • 不可变对象:所有字段 final,确保数据一致性
  • 时间原生支持:直接使用 DateTime,避免字符串解析开销
  • 唯一 ID 生成microsecondsSinceEpoch 保证会话内唯一性

🧩 领域驱动设计(DDD):
将业务概念(票据类型、有效期)直接映射为代码结构,降低认知负荷。


二、筛选系统:FilterChip 驱动的语义化导航

2.1 动态筛选逻辑

List<TicketItem> get _filteredTickets {
  if (_filter == TicketType.all) return _allTickets;
  return _allTickets.where((t) => t.type == _filter).toList();
}
  • 计算属性get 方法确保始终返回最新结果
  • 零拷贝优化all 模式直接返回原列表引用

2.2 FilterChip 的 UX 优势

FilterChip(
  label: Text(type.label),
  selected: _filter == type,
  onSelected: (selected) => setState(() => _filter = type),
  selectedColor: Theme.of(context).colorScheme.primary,
)
交互亮点:
  • 视觉反馈:选中项高亮主色,未选项灰底
  • 语义分组Wrap 布局自动换行,适配小屏
  • 无模态切换:点击即生效,无需确认按钮

Material Design 准则
FilterChip 专为“互斥或非互斥筛选”设计,比下拉菜单更直观。


三、表单设计:健壮的输入验证与用户引导

3.1 日期解析容错

try {
  final parts = _dateController.text.split('-');
  if (parts.length == 3) {
    expiry = DateTime(int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2]));
  }
} catch (e) {
  // 显示格式错误提示
}

在这里插入图片描述

  • 显式格式要求:强制 YYYY-MM-DD,避免区域设置干扰
  • 防御性编程:捕获 FormatExceptionNoSuchMethodError
  • 用户教育:提示文案包含示例(例如:2026-03-15

3.2 业务规则校验

if (expiry == null || expiry.isBefore(DateTime.now().subtract(const Duration(days: 1)))) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('有效期不能是过去日期')),
  );
  return;
}
  • 宽松边界:允许“今天”作为有效期(subtract(1 day)
  • 即时反馈:错误信息精准定位问题

3.3 对话框状态隔离

void _openAddDialog() {
  _nameController.clear();
  _dateController.text = '';
  _selectedType = TicketType.movie; // 重置默认值
}
  • 避免状态污染:每次打开对话框重置表单
  • 提升可预测性:用户不会看到上次输入残留

四、状态感知 UI:有效期驱动的视觉编码

4.1 智能颜色映射

Color _getExpiryColor(DateTime expiry) {
  final diff = expiry.difference(now).inDays;
  if (diff < 0) return Colors.grey;   // 已过期
  if (diff <= 3) return Colors.red;   // 即将过期
  return Colors.green;                // 有效
}

在这里插入图片描述

视觉层次:
状态 颜色 语义
已过期 灰色 失效、不可用
3天内到期 红色 紧急、需关注
正常有效 绿色 安全、可用

4.2 多维信息呈现

subtitle: Column(
  children: [
    Text('${ticket.type.label} · 有效期:${_formatDate(ticket.expiryDate)}'),
    if (isExpired) const Text('⚠️ 已过期', style: TextStyle(color: Colors.red)),
  ],
)
  • 类型+日期组合:一行展示核心元数据
  • 条件警示:仅当过期时显示警告图标
  • 格式标准化_formatDate 确保 2026-3-52026-03-05

🎨 信息设计原则
用颜色编码状态,用文字解释细节,二者互补而非重复。


五、工程亮点与最佳实践

5.1 轻量级状态管理

  • 单一状态源_allTickets 列表 + _filter 枚举
  • 局部更新setState 仅重建必要部分
  • 无外部依赖:不引入 Provider、Riverpod 等,保持教学友好性

5.2 示例数据注入

WidgetsBinding.instance.addPostFrameCallback((_) {
  if (_allTickets.isEmpty) _addSampleTickets();
});
  • 首次加载填充:提升新用户体验
  • 非阻塞初始化addPostFrameCallback 避免 build 阶段副作用

5.3 主题自适应

backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200]
  • 深浅模式兼容:FilterChip 背景色自动适配
  • 无障碍对比度:确保文本在任何背景下可读

六、克制式设计:为何不持久化?

6.1 产品定位清晰

  • 临时性工具:解决“此刻需要”的问题,而非长期归档
  • 隐私优先:敏感票据(如优惠码)不应留存设备
  • 降低认知负担:用户无需思考“要不要删旧数据”

6.2 技术权衡

  • Web 平台限制:localStorage 有大小限制且需用户授权
  • 开发复杂度:持久化需处理版本迁移、数据清理等
  • MVP 原则:先验证核心价值,再考虑增强功能

⚖️ 乔布斯式提问
“如果这个功能对核心体验没有增益,就砍掉它。”


七、进阶演进方向

7.1 功能增强

  1. 扫码添加
    • 集成 mobile_scanner 插件,扫描二维码自动解析
  2. 日历集成
    • 将票据有效期同步到系统日历,设置提醒
  3. 批量操作
    • 长按进入多选模式,支持批量删除/导出

7.2 技术升级

  1. 本地持久化(可选):
    // 使用 shared_preferences 或 hive
    final prefs = await SharedPreferences.getInstance();
    prefs.setStringList('tickets', _allTickets.map((t) => jsonEncode(t)).toList());
    
  2. 动画过渡
    • 添加 AnimatedList 实现删除/插入动画
  3. 搜索功能
    • 在筛选栏上方添加搜索框,支持名称模糊匹配

7.3 设计深化

  1. 卡片式布局
    • 为每张票据设计独立卡片,包含图标、二维码占位符
  2. 过期自动归档
    • 在列表顶部增加“已过期”折叠区
  3. 数据统计
    • 显示各类票据数量分布(饼图/柱状图)

结语:少即是多,克制即力量

《票匣》证明了:最好的工具,往往看起来“什么都没做”。它没有云同步,没有历史记录,甚至没有保存按钮——但它精准地服务于一个高频、微小却真实存在的需求。

在功能膨胀成为常态的时代,《票匣》是一次勇敢的减法实验。它提醒我们:技术的价值不在于它能做什么,而在于它选择不做哪些事

对于开发者而言,这不仅是一个票据管理器,更是一面镜子——照见我们是否真正理解用户,是否敢于对“加功能”的惯性说不。

“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”
—— Antoine de Saint-Exupéry

愿你的下一个应用,也能在喧嚣世界中,留下一片宁静的空白。


GitHub Gist 链接ticket_hub_app.dart
适用场景:临时票据管理、Flutter CRUD 教学、枚举驱动 UI 范例、FilterChip 实践

🧾 Happy Coding!
让每一行代码,都为用户的专注力护航。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐