1. 先从基本面拆需求:药箱管理加血糖记录到底要做什么
我最近做了一个跑在 OpenHarmony 设备上的家庭药箱管理 App,项目代号就叫 flutter_for_openharmony。起因很简单:家里药抽屉乱成一团,过期的药不敢扔又怕误吃;长辈每天测血糖,结果全记在纸上,偶尔想翻一下过去一周的趋势,根本翻不到。这个项目把两件事做成一个应用:药品信息录入和效期余量管理,以及血糖记录和趋势统计。这里我把从需求拆解到 Flutter 适配 OpenHarmony 的完整思路写下来,给正在做健康类跨平台应用的朋友做个参考。先说结论:这类项目最难的不是界面,也不是图表,而是把数据模型想清楚,再把平台差异控制住。
1.1 从一个使用场景推导功能边界
我不建议上来就设计云同步、多家庭、医生随访那种大而全的功能。先想清楚最真实的场景:某天早晨,老人打开药箱找不到药,打开手机能看到“阿托伐他汀还剩两片,3天后过期”;中午测完血糖,顺手输入一个数值,晚上能看到一周的趋势曲线。围绕这两个高频动作,往下拆功能清单。
功能清单大致包括:药品录入、编辑、删除、搜索;药品规格、剂量、库存数量、生产批次、有效期、分类;临期和低库存标记;用药提醒本地通知;血糖记录和修改;按时间展示历史列表;用折线图看血糖趋势,并对空腹和餐后数据分别统计。这是我第一版全部功能,够用而且没有盲目扩大。很多健康类 App 一上来就搞登录账号、云端病历夹,实际使用率并不高,家庭场景里“打开即记、本地优先”反而是最舒服的。
1.2 先做 MVP,功能优先级怎么排
如果一个人第一次接触 Flutter 和 OpenHarmony,建议把项目缩成一个最小可运行版本。我把功能分为三档:
| 优先级 | 功能 | 说明 |
|---|---|---|
| P0 | 药箱增删改查、血糖记录增删改查、本地持久化 | 没有这些,应用没有使用价值 |
| P1 | 临期提醒、低库存提示、血糖趋势图 | 让数据产生决策价值 |
| P2 | 用药提醒通知、导出 CSV、备份恢复 | 后期体验增强,可迭代上线 |
这种排序有实际考虑。药箱和血糖记录的核心是“不丢数据”,所以持久化一定放在第一版;提醒和图表属于交互增强,可以放到第二个版本;备份导出属于数据归属安全,等基础功能稳定再做。后面做的时候,我也是按这个节奏交付的,每个版本都能独立跑通测试。先能查、能记、能存,再去做提醒和图表,这样开发过程中每个里程碑都有可用产物。
2. 技术选型复盘:为什么 Flutter 能跑在 OpenHarmony 上
2.1 Flutter 在 OpenHarmony 上运行的基本原理
OpenHarmony 虽然有自己的应用开发框架,但 Flutter 这类跨平台方案也能直接落地。背后原理不复杂:Flutter 的 Dart VM 加载业务代码,UI 通过自绘引擎直接渲染到 OpenHarmony 提供的显示 Surface 上,不依赖系统原生控件;需要调用系统能力时,再通过 MethodChannel 走平台通道,让原生侧完成通知、存储、权限等动作。这就意味着大多数界面和业务逻辑代码可以跨平台复用,需要单独适配的主要是平台相关接口。
实际体验下来,这个方案在 UI 一致性上有天然优势。同一个页面,在 OpenHarmony 设备上和其他平台测试机上几乎长一个样,没有各端一到两像素偏差的问题。对我这种要同时维护多个目标的开发者来说,节省的时间非常明显。Flutter 的布局和渲染模型本身是自包含的,页面不会因为系统控件的风格变化而变形,这对健康数据展示类页面尤其重要:掉点、对齐、字体大小,都要稳定可控。
2.2 原生方案与其他跨端方案的取舍
简单对比一下几类选择:
- ArkUI 原生:对系统能力支持最直接,但需要重新学习方舟应用开发语言和布局体系,如果团队里已经有不少 Dart 代码,迁移成本偏高。OpenHarmony 原生生态在快速完善,组件质量和文档量都还不错,但要做出同样复杂度的血糖趋势图表和列表动效,成本并不低。
- Web/H5 混合:开发快,但在复杂交互和大量数据渲染时会有卡顿,而且离线能力和系统通知接入流畅度都弱一些。药箱数据属于本地高频操作,一旦断网导致页面不可用,体验会非常差。
- Flutter 自绘:学习曲线存在,但如果你熟悉 Dart,开发效率很高;缺点是插件生态在 OpenHarmony 上还不够全,个别三方库要自己封装。
我更倾向 Flutter 的理由还有一个,它的状态管理和组件化方式很适合“药箱列表 + 血糖趋势图”这种页面结构。同一个列表模型,可以通过一个 ChangeNotifier 让多组件联动刷新,减少手工 setState 带来的状态散乱。本身药箱页面同时要显示列表、搜索、预警卡片,如果靠一层层回调传递状态,后面维护起来会很痛苦。
2.3 标题里的 flutter_for_openharmony 是什么
这个代号是我给项目里那层 Flutter 与 OpenHarmony 桥接适配起的名字,不是官方 SDK。它封装了引擎初始化、Surface 接入、平台通道注册等步骤,让我在 Dart 侧可以用标准 Flutter API 写代码。实际工程中,这层适配的工作量也比较集中:要处理系统差异的主要是权限、存储目录、通知服务、日志输出和文件选择这些点。把这层做好,后面的业务开发才会顺畅。项目取这个代号,也是为了提醒自己不要忘记平台差异问题,所有涉及系统能力的代码,都要先经过这一层,而不是直接到处写平台判断。
3. 数据模型和项目结构:先建两张表再谈界面
3.1 药箱表和血糖记录表怎么设计
药箱管理用一张 medicine 表就够了,字段包括名称、规格、剂量、分类、批号、库存数量、单位、有效期、生产日期、备注、创建时间和更新时间。其中“分类”字段用来做慢性病药品、外用药、急救药等分组筛选;“有效截止日期”和“库存数量”是后面预警逻辑的关键。如果后续要加家庭成员维度,也可以再补一个 owner_id 字段,不同成员之间做数据隔离。
血糖记录单独建一张 glucose_record 表。字段有测量时间、血糖数值、单位、时段标签(空腹/餐后/随机)、备注、创建时间。为什么不把血糖数据塞进药箱表?因为维度完全不同:药品关心的是“还剩多少、是否过期”,血糖关心的是“何时测的、是否空腹、趋势怎样”,分开建表更符合查询场景。两表之间目前没有外键关联,业务上也不需要,保持简单即可。
数据库我用的是基于 sqlite 的适配方案,在 OpenHarmony 上通过插件能力访问本地文件。初始化代码大致如下:
class AppDatabase { static const _dbName = 'family_medicine_box.db'; static const _dbVersion = 1; Database? _db; Future<Database> get database async { if (_db != null) return _db!; _db = await _open(); return _db!; } Future<Database> _open() async { final dir = await getApplicationSupportDirectory(); final path = '${dir.path}/$_dbName'; return openDatabase(path, version: _dbVersion, onCreate: (db, version) async { await db.execute(''' CREATE TABLE medicine ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT, dosage TEXT, category TEXT, stock INTEGER DEFAULT 0, unit TEXT DEFAULT '盒', expire_date TEXT, manufacturer TEXT, note TEXT, created_at TEXT, updated_at TEXT ) '''); await db.execute(''' CREATE TABLE glucose_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, value REAL NOT NULL, unit TEXT DEFAULT 'mmol/L', period TEXT, measure_time TEXT, note TEXT, created_at TEXT ) '''); }); } }注意这里我用了 getApplicationSupportDirectory 获取目录,这个 API 在 Android 上返回应用私有目录,在 OpenHarmony 上则需要通过平台通道做一层转换。如果直接沿用旧平台写死的路径,很可能出现目录不存在或权限不足的问题。所以我在适配层单独封装了一个路径获取函数,确保所有数据库文件、导出文件的读写都落在同一个沙箱目录下,避免因为路径不一致导致升级后数据丢失。
3.2 目录结构和状态管理选型
项目代码我按数据层、页面层、组件层拆开,没有把逻辑全塞进 Widget 里。目录大概是:
lib/ main.dart models/ medicine.dart glucose_record.dart repositories/ medicine_repository.dart glucose_repository.dart pages/ medicine/ medicine_list_page.dart medicine_edit_page.dart glucose/ glucose_list_page.dart glucose_record_page.dart home_page.dart widgets/ medicine_card.dart glucose_trend_chart.dart stat_card.dart utils/ date_helper.dart db_helper.dart状态管理选的是 Provider 加 ChangeNotifier。操作逻辑很简单:药品列表页加载数据后放入 MedicineModel,页面用 Consumer 订阅;编辑器填写完成后调用 repository 保存,再触发 model 刷新。这样列表、搜索、详情卡片能同步更新。血糖页面则按时间范围加载记录,用一个独立的 GlucoseModel 管理。选择 Provider 而不是更重的状态管理框架,是因为这个项目里的共享状态不多,主要就是列表数据和当前筛选条件,Provider 的轻量特点刚好匹配,调试也比较直观。
3.3 数据版本升级的坑
第一次建表后,如果后面改动字段,不能直接删掉重建。我在数据库版本号上做了递增,并在 onUpgrade 里用 ALTER TABLE 补充字段。比如想给血糖表加一个“是否服药记录”字段,就把版本从 1 改成 2,执行类似下面的语句:
await db.execute('ALTER TABLE glucose_record ADD COLUMN medication_tag TEXT');这条没提前规划,很容易出现在真机上更新后闪退的情况。数据库升级的测试一定要做“旧表 -> 新表”的迁移验证,而不是每次卸载重装。因为用户已经积累了几百条记录,卸载重装等于数据全丢,这个体验对家庭健康场景来说是不可接受的。我把数据库迁移逻辑单独写在 MigrationManager 里,每次升级前先检查当前版本,按版本号顺序执行迁移语句,并把迁移过程包在事务里,任一语句失败就回滚,保证数据完整性。
4. 药箱管理核心功能实现:从表单到预警提醒
4.1 药品录入表单和校验逻辑
药品录入页主要包含名称、规格、剂量、分类、库存、单位、有效期、厂家、备注。名称是必填字段,库存我限定为整数且不能为负数。有效期需要用日期选择器选择,不能直接让用户随意输入字符串,否则后面的临期计算会出问题。提交前做一个轻量校验:
String? validate() { if (name.trim().isEmpty) return '药品名称不能为空'; if (stock < 0) return '库存不能是负数'; if (expireDate == null) return '请选择有效期'; if (DateTime.parse(expireDate!).isBefore(DateTime.now())) return '有效期不能早于今天'; return null; }严格限制日期后,药品列表、临期预警和血糖记录的时间维度才有一致性。所有日期在写入数据库前统一转成 yyyy-MM-dd 或毫秒时间戳,展示层再做格式化,这样后面无论是排序还是统计,都不会因为格式不一致出偏差。这里还要注意时区问题,日期选择器拿到的本地日期,直接格式化保存即可,不要二次转时区,否则容易出现“今天选的有效期,存入后变成昨天”的诡异情况。
4.2 列表展示与状态标签
药品列表我用 ListView.builder 渲染卡片。每张卡片显示药品名称、规格、剩余数量,以及一个根据有效期计算出来的状态标签。状态分三种:正常、即将过期、已过期。判断逻辑写成一个独立函数,方便单元测试:
String expiryStatus(String expireDate) { final now = DateTime.now(); final date = DateTime.parse(expireDate); final days = date.difference(now).inDays; if (days < 0) return '已过期'; if (days <= 30) return '临期'; return '正常'; }注意 date.difference 的结果要按天数归一化,不能直接拿毫秒转数字去比较,否则会受当天时分秒影响。我建议把所有日期都先规范成零点再比较,也就是用 DateTime(year, month, day) 构造一个日期对象,再计算差值。这样今天到明天永远是 1 天,不会因为当前时间是下午一点还是晚上八点产生偏差。卡片布局上,我用了 Stack 在右上角放状态标签,过期标红、临期标黄,正常标绿,颜色信息配合文字,避免单纯靠颜色无法区分。
4.3 临期和低库存提醒怎么触发
预警最好的触发时机是首页刷新。每次进入首页时,我会遍历药品列表,找出已过期、未来 30 天内过期、库存低于设定阈值的药品,把它们统一显示在“需要关注”区域。这个方案简单可靠,不需要额外写复杂的调度器。阈值在设置里可配置,默认库存低于 3 盒或 3 片时标红。如果药品数量比较多,也可以把这部分查询交给 SQL,用 WHERE expire_date BETWEEN 和 stock <= 条件控制,返回速度很快,而且只查需要的字段,内存占用小。
用药提醒依赖本地通知。OpenHarmony 的通知能力和其他平台不完全一样,我在 Dart 侧定义一个 NotificationService,底层用 MethodChannel 调用原生接口。发送提醒前需要先检查是否已经授权,未授权则引导用户在设置里打开。通知内容会带上药品名称和剩余数量,提醒文案写得具体一点,比如“阿托伐他汀还剩 2 片,请记得服用并补药”,比一句“你有用药提醒”有用得多。
5. 血糖记录功能实现:从输入到趋势图
5.1 记录表单和数据校验
血糖记录页面里的核心字段是血糖值、测量时间和时段标签。血糖值我用数字键盘输入,范围限制在 2.0 ~ 25.0 mmol/L 之间,过高的异常值会弹提示,建议复核或去医院检查。这里要特别说明,App 只做记录和趋势参考,不能替代专业血糖仪和医生诊断,所以页面上我还加了一行小字备注,提醒用户如果连续出现异常值,应该尽快咨询专业人员。
时段标签分为空腹、早餐后、午餐后、晚餐后、睡前、随机。同样的血糖值,空腹和餐后的意义完全不同,统计时不能混在一起看。我在模型里用一个字符串字段保存,而不是用一个整数枚举,原因是后续要扩展中国式餐后两小时等场景,字符串字段的兼容性更好,查询时直接按字符串过滤即可。表单提交前会校验数值是否在范围内,测量时间是否晚于创建时间,以及备注长度是否合理,所有校验通过后才写数据库。
5.2 历史记录列表如何组织
记录列表按时间倒序展示,每一项显示测量时间、血糖值和时段标签。为了缩短每天看到的列表长度,我做了按天分组,每天显示一条日期头,底下是该天的多条记录。分组逻辑不复杂,从查询结果里按日期字符串切分即可。编辑可以长按记录弹出操作菜单,或者右滑出现编辑、删除按钮,点击后进入原表单回填数据。删除前弹确认框,避免误触。
列表项里还加了一个平均值小组件,当某一天的记录超过一条时会自动算当天平均,显示在日期头右侧。这个细节看着小,但对老人和子女都很有用,因为一天多次测量的情况并不少,只看单点容易产生误判。按天分组后,列表长度也被控制住了,不会因为一个月几百条记录导致 ListView 一次性渲染太多节点,性能问题在数据量变大之前就被提前规避了。
5.3 用 CustomPaint 自己画趋势折线图
本来想直接引用第三方的图表库,但在 OpenHarmony 适配过程中发现有两个问题:一是包体积增加明显,二是部分图表库依赖的轻量级手势组件在目标设备上会有兼容问题。于是血糖趋势图我直接用 CustomPaint 实现,效果完全够用。绘制思路分三步:计算数据范围,绘制坐标轴和辅助线,绘制折线和数据点。x 轴是测量时间点,y 轴是血糖值。
为了让图表有实际参考意义,我额外画了两条浅色横向参考线,分别对应 3.9 mmol/L 和 6.1 mmol/L,这是常见的空腹血糖参考范围。Painter 核心代码如下:
class GlucoseChartPainter extends CustomPainter { final List<GlucoseRecord> records; final double normalLow; final double normalHigh; GlucoseChartPainter({required this.records, this.normalLow = 3.9, this.normalHigh = 6.1}); @override void paint(Canvas canvas, Size size) { if (records.length < 2) return; final minY = (records.map((r) => r.value).reduce(min) - 0.5).clamp(0, 30); final maxY = (records.map((r) => r.value).reduce(max) + 0.5).clamp(0, 30); // 画正常范围背景 final bgPaint = Paint()..color = Color(0x1A4CAF50); final lowY = size.height - (normalLow - minY) / (maxY - minY) * size.height; final highY = size.height - (normalHigh - minY) / (maxY - minY) * size.height; canvas.drawRect(Rect.fromLTRB(0, highY, size.width, lowY), bgPaint); // 画折线 final linePaint = Paint() ..color = Color(0xFF2E7D32) ..style = PaintingStyle.stroke ..strokeWidth = 2.5 ..strokeCap = StrokeCap.round; final path = Path(); for (var i = 0; i < records.length; i++) { final dx = i / (records.length - 1) * size.width; final dy = size.height - (records[i].value - minY) / (maxY - minY) * size.height; if (i == 0) { path.moveTo(dx, dy); } else { path.lineTo(dx, dy); } } canvas.drawPath(path, linePaint); // 画数据点 final pointPaint = Paint()..color = Color(0xFF1B5E20); for (var i = 0; i < records.length; i++) { final dx = i / (records.length - 1) * size.width; final dy = size.height - (records[i].value - minY) / (maxY - minY) * size.height; canvas.drawCircle(Offset(dx, dy), 3, pointPaint); } } @override bool shouldRepaint(covariant GlucoseChartPainter oldDelegate) { return oldDelegate.records != records; } }正常范围背景色我用了透明度很低的安全绿,既不影响数据点阅读,又能一眼看出血糖是否高出正常区域。记录少于两个点时直接不绘制,避免除以零。这里有个小细节,x 轴并不是按真实时间等距排列,而是按记录序号均匀排列。这样做的原因是血糖测量时间并不规律,有时候一天测五次,有时候三天测一次,如果严格按时间比例绘制,数据点会挤在一团,根本看不清。治疗趋势本身更关心顺序和方向,所以按记录序号排列是一个更实用的折中方案。
图表下方再放三个统计卡片:平均血糖、最高血糖、最低血糖和达标率。达标率定义为数据值落在 3.9~6.1 或目标区间内的比例,计算时要去掉空值,随机时段的记录不参与空腹达标判断。统计卡片我用一个横向 Row 排列,视觉上清爽,而且不占太多垂直空间。对于家庭使用者来说,一个“最近 14 天空腹平均水平”的总结,往往比几十个原始数据点更有指导意义。
6. OpenHarmony 适配全程复盘:权限、存储、通知和打包
6.1 权限与沙箱目录的区别
Flutter 项目跑在 OpenHarmony 上,不要默认所有参考其他平台的权限配置都有效。OpenHarmony 的应用配置文件通常是 module.json5,需要在这里声明要访问的权限,例如读取存储、通知、网络访问等。声明完成后,部分权限还需要在运行时动态向用户申请。否则你会发现,在参考其他平台时很正常的数据导入流程,在 OpenHarmony 上会静默失败,甚至完全无回调。
本地数据库路径不要写死成常见移动系统里的 data/data 路径。OpenHarmony 对应用沙箱目录有自己的一套规则,最靠谱的做法是通过平台通道向原生侧拿到系统返回的应用支持目录,再拼上数据库文件名。我在前面的 AppDatabase 里就是用 getApplicationSupportDirectory 而不是硬编码路径。目录相关代码在适配层统一处理,业务层完全感知不到路径差异,后面如果要迁移到新的系统版本,也只需要改适配层。
6.2 通知服务的平台通道示例
用药提醒和血糖测量提醒都需要系统通知。Dart 侧定义一个 MethodChannel 服务,调用原生方法发送通知:
class LocalNotificationService { static const channel = MethodChannel('family_medicine/notification'); Future<void> showMedicineReminder(String title, String body) async { await channel.invokeMethod('showReminder', { 'title': title, 'body': body, 'triggerTime': DateTime.now().add(Duration(minutes: 1)).millisecondsSinceEpoch, }); } }原生侧收到消息后,通过 OpenHarmony 的通知接口创建通知。这里重点是一个容易踩的坑:通知的触发时间一般要求是时间戳,时区要统一使用本地时区,不要内部转成 UTC 再传回去,否则会发现通知提前或延后一个小时。我就在这上面调了整整一个晚上。建议所有时间字段都用毫秒时间戳传,展示层再做格式化,避免跨平台时 DateTime 序列化和反序列化出现时区错位。
通知功能除了提醒本身,还要处理重复点击的问题。用户在药箱页面点了“已服用”按钮后,我通过另一个接口取消尚未触发的通知,避免老人已经吃完药却再次收到提醒。这个交互虽然是边缘场景,但直接影响信任度,如果总是误报,家人很快就不再依赖这个提醒功能了。
6.3 字体、体积和调试注意事项
OpenHarmony 设备的中文字体渲染并不是每个版本都一致,个别系统裁剪版本缺省字体文件,会造成 Text 显示为方框。解决办法是在 Flutter 侧为 MaterialApp 统一配置 fallback 字体,把常用的开源中文字体打包进 assets,或者至少设置 fontFamilyFallback 列表,指定一个系统支持的字体名。我在项目里选择直接打包一款字体文件,虽然增加了一点应用体积,但换来了稳定一致的渲染效果,尤其在药品名称这类关键信息上,不能出现乱码。
包体积方面,Flutter 产物本身比小型原生应用大不少,如果项目对包体敏感,需要裁剪未使用的 native library,并在 release 构建时开启 tree shake icons,同时检查 assets 里是否混进了不必要的测试资源。调试上,不要完全依赖热重载。我在一些开发板上测试时,热重载偶尔会失效,页面状态停留在旧代码,这时先停掉应用再重新构建即可,别浪费时间去猜环境问题。真机日志输出也可能缺失,我习惯在关键操作里主动加日志,并统一输出带时间戳的信息,方便回溯。
6.4 分发和真机安装的注意事项
OpenHarmony 设备安装 App 的签名要求与常见移动系统类似但不完全相同,需要在工程里配置好签名能力。首次安装到开发板时,如果签名不匹配,应用能装上但无法访问部分敏感权限。我建议准备一台专用测试设备,反复测试“全新安装、升级安装、覆盖安装”三种方式,重点看数据库迁移和权限授权是否会丢失。升级安装尤其重要,因为正式使用后家人不会每次点“卸载重装”,他们只会点“更新”,如果升级路径没测好,所有积累的药箱数据和血糖记录都可能消失。
另外,OpenHarmony 不同设备的内存和性能差异比想象中大。有的开发板内存不到 2GB,跑完整 Flutter 应用虽然能进界面,但列表滑动时会明显掉帧。我在最终版本里把列表项图标全部简化为线条风格,并把阴影改为轻量边框,就是为了在低端设备上也能顺畅操作。这个优化不会影响功能,但对实际体验影响很大。
7. 问题排查与优化建议速查
7.1 实际运行中的典型问题
把我在开发过程中遇到比较典型的几个问题列成表,方便直接对照:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 数据库打开失败 | 沙箱目录不对或首次初始化权限未授予 | 用平台通道获取真实支持目录,申请存储权限 |
| 药品列表滚动卡顿 | list item 有重型布局或频繁 setState | 缩小卡片布局层级,列表项用 const 构造,局部刷新 |
| 血糖图不显示 | 记录少于两个或 y 轴范围计算除零 | Painter 判空,范围做 clamp |
| 本地通知不触发 | 权限未申请或时间戳时区不对 | 检查通知授权,统一用本地毫秒时间戳 |
| 升级后闪退 | 数据库表结构没迁移 | 递增版本号,写 onUpgrade,覆盖旧数据测试 |
每一个问题我都实际撞过。尤其是数据库迁移,表面上只是加了一列,但如果用户手机上已经存在旧版本,直接跑新代码就会报 no such column。后来我把 onUpgrade 的语句统一放到事务里执行,并把所有迁移操作都写进 MigrationManager,再在每个版本升级前做自动备份,这样出问题时还能从备份恢复。迁移脚本不是一次性写完就不管,只要后续加字段、加表、改索引,都要在这里补一条迁移记录。
7.2 性能优化建议
数据量大了以后,最明显的瓶颈是列表重建。建议使用 ListView.builder 加分页,一次只加载当前屏幕需要的记录;数据库查询不要把所有字段都取回来,只取展示需要的字段;如果同一页面有多个子 widget 依赖同一个 model,可以拆成多个 Consumer 而不是一个大的 provider 包裹,这样局部刷新更高效。图表部分如果记录超过几百条,可以在 Painter 里做数据降采样,比如每隔 3 个点取一个极值点,曲线形状影响不大,CPU 占用能降一半。
本地数据库还有一个容易忽略的优化点:索引。血糖表如果经常按 measure_time 排序,就给 measure_time 建一个普通索引;药箱表如果经常按 expire_date 筛选今天到期或近期到期的药品,也给 expire_date 建索引。加了索引之后,数据量到几千条时查询速度依然能保持在毫秒级,列表滚动也不会因为等待数据库查询而卡住。
7.3 后面还能怎么做
这个项目做下来,我发现家庭药箱和血糖记录还有很多可扩展空间。比如为每一位家庭成员建立独立数据空间,记录各自的药品和血糖数据;增加 CSV 导出,方便去医院时直接给医护人员看趋势;后续可以做用药依从性分析,统计按时服药比例和漏服率。对那些已经有数据积累的家庭来说,这些功能能带来的实际帮助比又一个购物式应用更有价值。
还有一个很实用的方向:把药箱和血糖联动起来。比如记录血糖的时候可以顺手标记“是否已服降糖药”,当血糖趋势持续异常时,能辅助观察用药和血糖之间的关系。这个功能不需要复杂的算法,只是在血糖记录表里多一个字段,然后增加一个筛选维度即可。但跨表的数据联动对数据模型和查询设计有要求,所以我建议在基础版本稳定后再加,一步一步来。
我个人在这个项目里最深的体会是:技术选型再花哨,也不如先把核心流程跑通。flutter_for_openharmony 这个项目最值的部分,不是把界面做得好看,而是把药箱数据和血糖数据从纸质记录盘活成了可查询、可预警、可看趋势的本地数据库。如果你也想做类似的东西,我的建议是从一张药箱表和一张血糖表开始,先让家人真正用起来,再慢慢加功能。数据一旦积累起来,应用的不可替代性就出现了。最后再分享一个小技巧:开发这类健康管理工具,一定要留好数据导出入口,无论用 CSV 还是 JSON,至少要让用户能把数据拿走。这不只是为了合规,更是对用户信任的基本尊重。