最近在整理 NoteStar 这个项目时,我把整套“便笺服务类”的设计思路和落地方案重新梳理了一遍。项目的定位非常明确:用 Flutter 在 HarmonyOS 6.0 上实现一个本地优先的便笺工具,能力范围涵盖笔记的创建、编辑、删除、归档、置顶、搜索和本地持久化。标题里那个“便单服务类”,在工程里我落地成了一套可复用的便笺服务层,页面不直接操作数据库,所有数据能力都通过 Service 接口暴露。这篇博文会把这套设计的选型、建模、实现、适配和踩坑经历完整记录下来,给正在做 Flutter 本地存储或 HarmonyOS 适配的人提供一个可直接参考的样本。
在往下写之前,先说明一个容易混淆的点:标题里的“便单服务类”不是“账单服务”,而是“便笺管理服务”的意思。这类应用的功能看起来很少,但真正动手做的时候你会发现,所有复杂度都藏在数据完整性里。用户记一条笔记只需要几秒钟,但这条笔记要经历创建、编辑、归档、恢复、搜索、备份这一整条链路,任何一环出了问题,用户都不会给你第二次机会。这也是我为什么要专门围绕“持久化存储和管理”来写这篇实践总结。
1. 项目概述:NoteStar 要解决什么问题
严格来讲,NoteStar 不是一个从零想象的“创新项目”,它更像是对经典场景的再一次认真表达:在手机上随手记录、管理、归档便笺。市面上的笔记类应用已经非常多,但真正能满足“离线优先 + 跨端一致体验 + 存储可控”这三个条件的,其实没有多少。很多产品要么把同步逻辑做得太重,要么把数据模型设计得过度抽象,根本撑不起一个“随手记”的轻量场景。NoteStar 想做的就是把这个场景的存储底座做扎实。
项目功能边界一开始就划得很清楚:
- 快速创建便笺,支持标题、正文、标签;
- 对已有便笺进行编辑、删除、置顶、归档和取消归档;
- 通过关键词搜索标题、正文和标签;
- 所有数据本地持久化,App 重启后数据不丢;
- 提供本地备份与恢复能力,防止误删导致的数据不可逆。
这条功能清单看起来非常简单,但落到技术上,每一步都要做取舍。比如主键到底用自增 ID 还是 UUID?时间字段存字符串还是时间戳?标签是单独建表还是冗余存储?归档和置顶要不要应用在同一条排序规则里?这些细节决定了应用的长期可维护性,也决定了用户数据的可靠程度。
我在动手写代码前做的第一件事,不是创建 Flutter 工程,而是把上面这些问题先逼问一遍。这个习惯来源于一次教训:早年间我做小工具应用,习惯边写边想数据结构,结果功能上线三个月后想加一个“归档”能力,发现数据库表结构完全撑不住,推倒重来了一次。这次做 NoteStar,我宁可前期慢一点,也要先把模型和服务边界定义清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计与技术选型:为什么把服务层单独抽出来
2.1 从页面直连数据库到 Service 层
很多 Flutter 新人写便笺应用,最常见的写法是在页面 State 里直接调用数据库方法,比如在 _onSave 回调里执行 db.insert()。这个写法在功能少于 10 个的时候非常爽,代码看起来直截了当。但一旦功能超过 20 个,你会发现自己陷入重复代码的泥潭:每个页面都要处理数据库初始化、事务、异常回滚;数据库模型一旦改动,所有页面同步修改;想在页面上加一个统一的“保存成功”提示,要到处复制粘贴。
NoteStar 从第一天起就切成了三层结构:
- UI 层:只负责展示和交互,页面里看不到任何 SQL 语句;
- Service 层:也就是便笺服务类,对外暴露
createNote、updateNote、deleteNote、searchNotes、archiveNote这类语义化方法; - Store 层:实现真正的持久化操作,包括数据库建表、CRUD、文件备份等。
这么设计的理由很直接。便笺应用的核心资产是数据,数据访问逻辑需要高度稳定;而 UI 恰恰是改动最频繁的部分。把这两者切开,UI 改版不影响存储,存储迁移不影响 UI,后续哪怕要把 SQLite 换成本地 JSON 文件,也只需要改 Store 层的内部实现,Service 的接口签名完全不用动。
有读者可能会问,功能这么少,有必要做三层吗?我的回答是:有必要。因为持久化存储和 UI 是完全不同的关注点,存储关注数据一致性,UI 关注交互反馈。如果把它们耦合在一起,你每次改 UI 都要担心会不会破坏数据库逻辑,每次调数据库又不敢动页面。这种隐性成本在项目前期几乎看不到,但到后期债务积累起来,速度会非常快。
2.2 数据模型设计:别把便笺设计成一张“死表”
便笺模型在设计时要同时兼顾两类诉求:列表页展示要快,编辑页读写要准。我最终定义的 notes 表结构如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | TEXT | 主键,用 UUID 字符串,避免自增 ID 在数据合并时冲突 |
| title | TEXT | 便笺标题 |
| content | TEXT | 正文内容,支持纯文本或轻量 HTML |
| tags | TEXT | 标签列表,用 JSON 数组字符串保存 |
| is_pinned | INTEGER | 是否置顶,0/1 |
| is_archived | INTEGER | 是否归档,0/1 |
| created_at | INTEGER | 创建时间,Unix 毫秒时间戳 |
| updated_at | INTEGER | 最后修改时间 |
| reminder_at | INTEGER | 提醒时间,可为空 |
很多人会把主键直接定义成 INTEGER AUTOINCREMENT,但我在这里特意用了 UUID。原因是,如果后期考虑多设备或数据合并,自增 ID 几乎必然冲突。在 NoteStar 这种本地优先的架构里,单机 UUID 的开销可以忽略不计,但换来的是以后接云同步时不用改表结构。
时间字段我统一用毫秒时间戳,而不是 ISO 8601 字符串。这个决策基于一个很简单的理由:存储层永远存机器友好型数据,人看的格式化逻辑放在 UI 层处理。时间戳在排序、范围查询、时间计算上比字符串快得多,也不会因为时区不同产生排序错乱。
标签字段用 JSON 数组字符串保存,而不是单独建一张标签表,也是基于场景做的取舍。NoteStar 的单条笔记标签数量极少,单独建表意味着每次查询都要 JOIN,带
