1. 开发助手App的"数据底盘",决定功能的上限
把 Flutter 应用跑到 OpenHarmony 上,再把应用定位成"软件开发助手",这两件事叠加在一起,最先考验你的不是 UI 怎么写,而是数据模型怎么设计。我在做这个App时,第一步落地的就是整套实体模型:项目、任务、代码片段、标签、偏好设置,它们之间的关联关系一旦定错,后面每个功能都会在你最想不到的地方反噬你。
这篇文章不讲渲染性能、不讲环境搭建,专门聊数据模型设计这件事:为什么要这样拆实体、Dart 代码里怎么组织这些模型、和 BLoC 状态管理怎么配合、在 OpenHarmony 上如何做持久化和迁移。适合两类人看:一是准备用 Flutter 开发 OpenHarmony 应用的开发者,二是想把业务模型做得更抗变的 Flutter 工程师。
1.1 这类App的数据特征,和普通业务App有什么不同
普通业务App的领域模型通常很"顺":订单、用户、商品,关系一眼能看清。开发助手App完全不是这样,它更像一个"开发者的第二大脑":
- 数据量大但零散:代码片段、技术笔记、报错记录、依赖包信息,每一条都不大,但数量可以积累到几千上万条;
- 实体间关系复杂:一个项目下面挂多个任务,任务关联代码片段,代码片段打多个标签,收藏可能同时指向项目、文章、工具三种类型;
- 强离线诉求:很多开发者打开这类工具时是在写代码的间隙,不保证有网,所有数据必须在本地可用、可搜索、可修改;
- 需要跨端一致:代码最终可能同时在 OpenHarmony 设备、Android、桌面端跑,模型不能绑死某个平台的存储实现。
这意味着数据模型不能只满足"当前页面能显示",它要承担三件事:作为功能之间的通信契约、作为本地持久化的结构基础、作为未来新功能扩展的锚点。如果你只是给页面写几个临时 Map,功能可能三天就能跑通,但一个月后你会被各种字段不一致拖死。
我见过不少项目就是死于从第一天起不重视数据模型:同一个"项目状态"在一个文件里是 int,在另一个文件里是 String;"收藏时间"一个用毫秒时间戳,一个用 ISO 字符串。这种隐患不像崩溃那么显眼,但排查起来极其耗神,尤其当你在 OpenHarmony 上需要对接不同存储引擎时,字段语义不一致会直接放大成本。
1.2 OpenHarmony + Flutter 组合给模型设计加了哪些约束
Flutter 在 OpenHarmony 上跑起来之后,有两点和 Android/iOS 明显不同,建模时得提前考虑进去。
第一,插件生态还在快速补课期。你常用的 sqflite、shared_preferences 这些插件的 OpenHarmony 适配版本,行为和原平台并不完全一致,而且部分 API 能力还在演进。如果业务代码直接依赖某个具体插件的类型,一旦换了实现或升级版本,改动的文件会非常多。正确的做法是把存储操作藏在仓储接口后面,模型层完全不感知到底用的是关系型数据库还是键值存储。
第二,序列化和反序列化的开销需要认真对待。开发助手App里会有大量列表页、搜索页,每次从存储读取一批数据回来,都要走 fromJson 到实体对象的转换。如果模型字段嵌套过深、或者每个对象里塞了一堆大字段(比如整段代码文本),列表滑动时的重建和 BLoC 的 state 比较都会受到牵连。我看到有团队为了图省事,把整个大模型对象扔进 state 里,结果每次 equatable 比较都要深比较一串集合,帧率直接从 60 掉到肉眼可见的卡顿。后面讲状态管理时我会专门说怎么裁模型。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实体划分:开发助手里到底有哪些领域模型
先别急着写类,把实体划清楚比写代码重要得多。我第一版设计时也犯过"想到什么加什么"的毛病,最后字段彼此矛盾,重构了两轮才稳定下来。经过取舍,我把整个App收敛成了五个核心实体:DevProject、DevTask、CodeSnippet、Favorite、AppSettings,其余都算它们的附属。
2.1 项目和任务:带状态的骨架
项目是最高层的聚合根,所有任务和片段都可以归属到项目下。这个实体的设计有三个关键决策:
第一,ID 用什么。我选了 UUID 字符串而不是自增数字。原因很直接:本地新建的项目可能还没有同步条件,要有全局唯一标识才能避免后续做导入导出、多端同步时撞 ID。模型里凡是涉及关联的字段,都用这个字符串 ID 而不是对象引用。
第二,状态字段用枚举而不是布尔。项目不是只有"进行中/已结束"两个状态,实际会有 active、completed、archived 三种,归档和完成在业务上完全不是一个含义。用枚举配合 switch 处理,比到处都是 isDone 的布尔判断清晰得多,后面做筛选时也不会漏分支。
dart复制enum ProjectStatus {
active,
completed,
archived;
static ProjectStatus fromName(String name) =>
values.firstWhere((e) => e.name == name,
orElse: () => ProjectStatus.active);
}
第三,时间字段必须成对出现。createdAt 记录创建时间,updatedAt 在每次修改时刷新。这不是模板洁癖,开发助手类App未来做版本对比、做"最近修改排序"、做跨端合并,没有时间戳什么都做不了。我见过不少人的模型只留一个时间,结果后面做同步时无从判断新旧。
任务实体和项目不同,它多了一个 orderIndex 字段,用于在列表里手动排序。这里要提醒一句:排序字段不要叫 sort 或 order,后面对接数据库或和平台关键字打交道时容易出问题,具体我放到最后一章详细说。
2.2 代码片段与标签:内容型数据怎么建模
代码片段是这种App里最"肥"的实体。除了常规的 id、title、content、language,我最终保留了 sourceProjectId(可空)、favorite(是否收藏)、tagIds(标签 ID 列表)、description 这几个字段。
内容本身用纯文本存储,不额外拆段。有些工具会想把代码块做结构化(比如记录行号、高亮样式),我的建议是千万别。行号和样式属于展示层的东西,存到模型里会让每一个对该字段的消费方都背上负担。真要展示高亮,让 UI 层根据 language 和 content 现场做,模型保持干净。
标签这里有个容易被低估的坑:标签和代码片段是多对多关系,但为了省事,很多人直接在模型里用 List<String> 存标签名。短期没问题,长期会出两件烦心事:改名不方便(所有片段里的字符串都要改)、无法记录标签本身的元数据(比如使用次数、颜色)。我后来拆出了一个 Tag 实体,片段模型里只存 tagIds。虽然首次建模多写了一点代码,但后面做标签筛选、标签管理页时非常省心。
json复制{
"id": "b3a1c09d-2e4f-4f6e-8a3d-9b0a2f6c1d7e",
"title": "OpenHarmony 上读取系统版本",
"content": "ohos.system.osSystemInfo...",
"language": "dart",
"sourceProjectId": "9c2e0f1a-5c4b-4b73-9d1a-2b7e8c3a0f44",
"favorite": false,
"tagIds": ["openHarmony", "dart", "deviceInfo"],
"createdAt": "2024-06-01T10:00:00.000Z",
"updatedAt": "2024-06-01T10:00:00.000Z"
}
2.3 收藏、历史与偏好设置:轻量模型的坑
收藏实体最容易设计翻车。因为它要收藏的对象不止一种——可能收藏一个项目、一条代码片段、一个外部工具链接。我见过两种常见做法:一种是建多个收藏表,每种类型一套;另一种是用一个字段存"类型+ID"的字符串组合。
我用的是一张统一收藏表,靠 entityType 和 entityId 两个字段做多态关联。这样做的收益很直接:收藏列表页只需要查一张表,UI 层再根据类型分发到不同详情页,不需要 union 查询。
dart复制class Favorite extends Equatable {
final String id;
final FavoriteEntityType entityType; // project / snippet / tool
final String entityId;
final DateTime createdAt;
@override
List<Object?> get props => [id, entityType, entityId, createdAt];
}
偏好设置则相反,不要拆成一张张表。设置项五花八门:主题、默认代码语言、是否显示行号、历史记录条数……它们的读多写少、结构松散,最适合用键值对存储,模型层只定义一个 AppSettings 聚合对象,或者干脆用几个独立字段对应底层 KV。过度建表在这里只会增加无意义的 JOIN 和维护成本。
实体划分阶段我得到的经验是:聚合根少一点,关联用 ID 而不是引用,列表字段只存 ID。这三点做到位,后续的仓储层、BLoC 层都会顺很多。
3. 在 Dart 里落地模型:选型和组织代码的经验
实体规划好之后,进入写代码阶段。这一章聊的是 Dart 层面的实操:不可变模型、序列化工具怎么选、part / part of 怎么用。这些决策看起来是小事,但它们决定了你加字段时是"改一个文件"还是"改八处地方"。
3.1 不可变模型为什么值得一开始就坚持
我在给开发助手App写模型时,第一原则就是:字段全部 final,修改一律通过 copyWith。这是我从早期踩坑换来的教训。曾经图方便把模型写成可变的,结果 BLoC 里的事件在传递过程中被某个 widget 顺手改了字段,界面跳变了但调试半天找不到是谁改的。不可变模型从根上消灭了这类问题。
配合不可变模型的两个标配是 equatable 和 copyWith。equatable 解决了状态对比的痛点;copyWith 则提供了安全的修改路径。手写 copyWith 时有个细节要注意:可空字段用"参数为 null 就不覆盖"的写法,会带来一个副作用——你没办法把一个字段从非空改成 null。解决办法是引入哨兵对象或者用 freezed 的 @Freezed(copyWith: ...) 配置,我在中大型项目里更推荐直接上 freezed。
dart复制class DevProject extends Equatable {
final String id;
final String name;
final String? description;
final ProjectStatus status;
final DateTime createdAt;
final DateTime updatedAt;
const DevProject({
required this.id,
required this.name,
this.description,
this.status = ProjectStatus.active,
required this.createdAt,
required this.updatedAt,
});
DevProject copyWith({
String? name,
String? description,
ProjectStatus? status,
DateTime? updatedAt,
}) {
return DevProject(
id: id,
name: name ?? this.name,
description: description ?? this.description,
status: status ?? this.status,
createdAt: createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
List<Object?> get props => [id, name, description, status, createdAt, updatedAt];
}
3.2 freezed 还是 json_serializable:我的选择
Flutter 生态里做模型代码生成,主流两套:json_serializable 和 freezed。不少教程会把它们讲成二选一,其实不是,freezed 底层就是依赖 json_serializable 的。我最后的方案是:实体模型用 freezed,轻量 DTO 如果逻辑特别简单就手写。
给几张表对比一下:
| 维度 | 手写 | json_serializable | freezed |
|---|---|---|---|
| 样板代码量 | 多 | 中 | 少 |
| copyWith 自动生成 | 需手写 | 不生成 | 自动生成 |
| 深比较支持 | 需手写 | 不涉及 | Equatable 集成 |
| 序列化支持 | 手写 | 支持 | 支持 |
| 依赖复杂度 | 低 | 低 | 中(含注解处理器) |
| OpenHarmony 构建兼容 | 无额外风险 | 常规 codegen | 常规 codegen |
在 Flutter for OpenHarmony 环境下,freezed 这类编译期生成工具没有特殊兼容问题,因为它和平台无关,只是 Dart 代码生成。真正要注意的是:生成的文件在 build.yaml 里如果和 Flutter 插件冲突,会报一些莫名其妙的错误。我的做法是把所有模型文件独立放在 lib/domain/models/ 目录,并且保证生成的 .freezed.dart、.g.dart 不跨目录分隔,否则 part 声明会随目录变化翻车。
一个用 freezed 的模型长这样:
dart复制import 'package:freezed_annotation/freezed_annotation.dart';
part 'dev_project.freezed.dart';
part 'dev_project.g.dart';
@freezed
abstract class DevProject with _$DevProject {
const factory DevProject({
required String id,
required String name,
String? description,
@Default(ProjectStatus.active) ProjectStatus status,
String? techStack,
required DateTime createdAt,
required DateTime updatedAt,
}) = _DevProject;
factory DevProject.fromJson(Map<String, dynamic> json) =>
_$DevProjectFromJson(json);
}
3.3 用 part / part of 组织模型文件的原则
part 在 Flutter 里的用途,一种是配合代码生成,另一种是手动组织聚合文件。我用的是后者:把一组关联实体写进一个聚合文件,用 part of 让它们共享文件的私有符号。这和代码生成产生的 part 容易混淆,我第一次搞混时还被编译器的报错折腾了半天。
组织原则很简单:领域边界一致的一组实体放一起。我把 DevProject、DevTask、ProjectStatus 放进 project_entities.dart,这个文件底部声明 part of 'project_entities.dart' 的子文件才能访问它的私有字段。这么做的好处是:私有辅助函数不用为了被多个模型共享而改成公开,文件之间边界清楚。
dart复制// domain/models/project_entities.dart
part 'dev_project.dart';
part 'dev_task.dart';
part 'project_status.dart';
// domain/models/dev_project.dart
part of 'project_entities.dart';
enum ProjectStatus { active, completed, archived }
class DevProject extends Equatable { ... }
需要留意的是:part of 的路径必须相对 import 正确,目录调整后第一件事就是跑到这些文件检查声明是否还匹配。另一个经验是,生成文件和手写文件不要混在同一个聚合文件里,各管各的目录,否则 build_runner 每次重建都可能在"找到多个同名文件"上纠缠。
4. 模型、仓储与 BLoC:数据在App里怎么流转
模型设计好后,它不会自己产生价值,必须被一个清晰的数据流用起来。我在这个App里用了经典的 BLoC 模式:View 发事件,Bloc 调仓储,仓储读写存储,结果通过 State 回传。这个链路里模型扮演的角色,远比"一个数据类"复杂。
4.1 仓储层把模型和存储解耦
这是我觉得整个架构里最值得投入的一层。开发助手App的数据来源未来可能有很多种:本地关系库、本地 KV、远端同步服务。一旦仓储层做得到位,换数据源就是换个实现类的事,模型、BLoC、UI 一行都不用改。
我定义的 ProjectRepository 接口只暴露领域模型:
dart复制abstract class ProjectRepository {
Future<List<DevProject>> fetchAll();
Future<DevProject?> findById(String id);
Future<void> upsert(DevProject project);
Future<void> delete(String id);
}
为什么要强调接口用领域模型而不是数据库实体?因为这样实现类就可以自由选择存储形态。我第一版在 OpenHarmony 上用键值存储直接序列化整个列表,后来数据量上来改成了关系型数据库,BLoC 层完全无感。如果你把 Map<String, Object> 或者数据库行对象直接透传到上层,等于让上层和存储绑死,后面必定后悔。
仓储层还有个容易被忽视的任务:处理模型和存储格式之间的转换。比如数据库里存的是整数状态码,而领域模型里是枚举,这个转换只能发生在仓储层内部,绝不能漏到 BLoC 里。
4.2 事件和状态里只放必要字段
BLoC 设计里最容易犯的错,是把整个实体直接塞进 Event 和 State。我早期就犯过:在 AddProject 事件里直接传 DevProject,在 ProjectListState 里直接挂 List<DevProject>。短看很爽,长看问题非常大。
第一个问题是 diff 成本。freezed 生成的 State 自带 == 和 hashCode,比较时会走实体内部的 props。如果把整个实体列表挂进去,每次一个字段变化(比如 updatedAt 刷新)都会让整个 State 判定为不相等,所有监听该 State 的 widget 全部重建。对开发助手这种列表页居多、数据量不小的App,直接表现为滑动掉帧。查问题时不一定能想到是模型比较的问题,但实际上就是它。
第二个问题是耦合。Event 里携带完整实体,意味着 UI 层必须有能力构造实体,这会让视图层知道太多领域逻辑。我的做法是:Event 只带轻量参数,State 只暴露视图需要的投影,真正的实体操作全部收在 Bloc 内部。
dart复制sealed class ProjectEvent {}
class LoadProjects extends ProjectEvent {
final ProjectStatus? filter;
}
class CreateProject extends ProjectEvent {
final String name;
final String? description;
final String? techStack;
}
至于从 DevProject 到视图模型的裁剪,我在列表页会做一个 ProjectListItemVM,只保留 id、name、status、updatedAt 四个字段。千万别把整个实体扔给列表 item,字段越少,重建越轻,ListView.builder 在 OpenHarmony 设备上跑起来也更稳。
4.3 Stream 里的模型要防"脏引用"
还有一个我说不清踩了多少次的坑:同一个实体对象在多个地方被引用,某处用 copyWith 创建了新对象,但另一处还持有旧引用。体现在界面上就是数据看起来"没刷新"。这里要分清两个概念:模型不可变保证的是对象内部不被改,不代表各层不会持有过期对象。
我的统一规则是:实体对象一旦出了仓储层,就按值传递,绝不长期缓存;所有状态变更必须通过 Bloc 重新走一遍加载流程。虽然多几行代码,但整个App的数据一致性有了保证。
5. OpenHarmony 上的持久化与模型版本迁移
模型在内存里跑通了,接下来要落到 OpenHarmony 的本地存储里。这一章讲我实际评估过的存储方案,以及一个很多教程不会认真讲的点:模型版本迁移。
5.1 三种存储方案怎么选
OpenHarmony 上本地数据存储,我最终评估集中在三种:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
ohos.data.preferences(键值) |
设置项、轻量标记 | 简单、读取快 | 不适合关系查询和大数据量 |
ohos.data.relationalStore(RDB) |
结构化业务数据 | 支持 SQL、索引、迁移 | 建表、迁移成本高 |
| 文件存储(JSON/二进制) | 导出导入 | 可读、易分享 | 查询能力弱,需全量读 |
我的分配是:设置和轻量标记走键值存储;项目和任务这类强关系、会频繁按条件筛选的数据走 RDB;代码片段因为内容是文本、且常作为整体读取,放在 RDB 里和文件方案都可以,我考虑到未来要做全文搜索,最终放在了 RDB 里。
这里要提醒一点:如果你使用 Flutter 的第三方插件在 OpenHarmony 上做关系型存储,别默认它的行为和 Android 上的 sqflite 完全一致。我在接入时发现部分插件实现里对事务、批量插入的支持还不够成熟,所以仓储接口里一定要预留好"批量写入"的抽象,必要时降级成逐条插入,避免上层逻辑被底层能力卡住。
5.2 模型版本号:从第一天就该有的东西
模型字段一定会增加,这是唯一确定的事。为了防止出现"A 版本写入的数据,B 版本读出来字段缺失"的问题,我在每个实体里都放了一个 modelVersion 字段,并且设计了一套简单的迁移机制。
dart复制class DevTask {
final String id;
final String projectId;
final String title;
final int orderIndex;
final int modelVersion; // 当前为 2
static const int currentModelVersion = 2;
}
迁移策略分两级:存储结构级迁移和数据级兼容。存储结构级迁移发生在 RDB 表结构改变时,比如新增一列,用 SQL 的 ALTER TABLE 完成;数据级兼容则靠 fromJson 里的默认值兜底。我见过一堆人只做了 SQL 迁移、忘了旧 JSON 数据还在本地,结果读旧数据时 as int 直接炸掉。安全的写法是解析老版本时给缺失字段补默认值,再在写入时统一升到最新版本。
dart复制factory DevTask.fromV1(Map<String, dynamic> json) {
return DevTask(
id: json['id'] as String,
projectId: json['projectId'] as String,
title: json['title'] as String,
orderIndex: (json['orderIndex'] as num?)?.toInt() ?? 0,
modelVersion: currentModelVersion,
);
}
这些迁移逻辑放在仓储层实现类里,不要让领域模型自己去判断。模型只负责表达"当前的结构",历史格式的兼容由仓储负责,职责才不越界。
5.3 时间字段和枚举的存储格式
时间字段我统一用 ISO 8601 字符串存。对比一下用 int 毫秒时间戳的写法,ISO 字符串人类可读、调试方便,JSON 序列化也自然。真正的坑是时区:如果你的App只在单机跑,UTC 和本地时间可能感受不到差异;一旦涉及多设备、云同步,所有时间都必须以 UTC 存储,展示时再由 UI 层转本地。我吃过这个亏:第一版直接存本地时间,换设备后记录的创建时间全乱了。
枚举类型存储时尽量存 name 字符串而不是 index 整数。index 的致命问题是:如果你在枚举中间插入一个新值,所有已经存储的数据的 index 都会错位,旧的 0 可能从 active 变成了 completed。虽然字符串占的空间大一点,但这个钱绝对值得花。读取端再配合 firstWhere 加 orElse 兜底,遇到未知值也能安全降级,而不是让整个反序列化崩溃。
6. 数据模型设计里的常见坑,和我的排错过程
这一章专门讲我在模型设计和落地过程中真实踩过的坑。每一个我都经历了从"怎么回事"到"原来如此"的排查过程,写出来希望能让你少走弯路。
6.1 字段名和平台关键字撞车
我最早给任务模型设计时,用了 order 做排序字段,结果在 OpenHarmony 的 RDB 建表时触发了一堆意想不到的问题。SQL 里 order 是保留字,你要么加反引号转义,要么换字段名。类似的高危字段还有 data、index、key、value、desc。
这个坑的诡异之处在于:不是每一条语句都会报错,有些查询在字段较少时侥幸绕了过去,一旦条件复杂就莫名失败。我最终的解决方案是在模型层就把这类字段全部改名,比如 orderIndex、payload、sortIndexKey,从源头杜绝问题。改字段名看着是小事,但它牵涉到序列化、数据库列名、BLoC 事件三个层级的同步修改,越早发现越省事。
6.2 集合字段的浅拷贝串扰
还有一个非常隐蔽的坑:copyWith 默认对列表字段做的是浅拷贝。换句话说,两个 DevProject 实例可能共享同一个 tagIds 列表对象。如果代码里哪个地方意外执行了 project.tagIds.add(...),你以为你在改新对象,其实把旧对象也改了。
排查这个问题的过程很典型:界面上某条数据的标签数量莫名其妙多了一个,每次重新进页面又恢复,但过一会儿又出现。看起来像随机 bug,实际是某个事件流里的局部修改污染了不可变对象。解决方式有两个:一是把所有集合字段的 copyWith 写成"重新创建列表";二是干脆统一用 freezed 生成的深拷贝行为。这里我强烈建议用后者,手写 copyWith 太容易漏。
dart复制DevProject copyWith({List<String>? tagIds}) {
return DevProject(
// ...
tagIds: tagIds ?? List.of(this.tagIds), // 关键:新建列表而不是直接赋值
);
}
6.3 模型膨胀:一次性字段的扩散
第三个坑是我在功能迭代中逐渐意识到的:模型里加字段太容易,删字段太难。为某个功能临时加的"是否展示引导"、"最后浏览时间",最后全都堆在主实体上,模型越来越臃肿,BLoC 每次状态对比的成本越来越高。
现在我的原则是:新字段如果不是所有消费方都需要,就不要放进核心实体。像"是否引导过"这种纯 UI 状态,放本地 KV 设置里;"最后浏览时间"这种展示性数据,放独立的历史表。核心实体只保留业务语义相关的字段。模型瘦身之后,序列化更快,状态比较更轻,OpenHarmony 上跑起来的流畅度也明显提升。
如果已经发现模型膨胀了怎么办?我上个月做了一件事:把 DevProject 里三个不常用的附加信息拆到了一个 ProjectMeta 实体里,用一对一关联。迁移过程并不复杂,但收益很实在——列表加载时不再需要解析一堆用不到的大字段。
6.4 一个完整的序列化故障排查示例
最后分享一个完整的排查过程,它包含了前面所有问题的一个缩影。某天测试发现:从任务详情页返回列表页时,偶发性的 _TypeError: type 'int' is not a subtype of type 'String?' 崩溃。
崩溃点是某个任务对象的 projectId。我一开始以为是数据库读出来的类型不对,查了 RDB 的建表语句,project_id 是 TEXT,类型没错。后来换了个思路,把崩溃时的 JSON 打出来,发现那条数据的 projectId 字段值竟然是 0。追溯写入路径,发现有两个模块都在创建任务:一个是新版代码,写 projectId 为 UUID 字符串;另一个是老模块还在用自增 ID 的本地临时项目,顺手把数字写进去了。
问题根因不是类型转换,而是同一个字段在不同写入路径的语义不一致。修复也不复杂:统一所有创建任务的入口,让仓储层强制校验 projectId 必须是合法 UUID,不合法就报错而不是静默落库。这件事给我最大的教训是:数据模型设计得再好,如果写入入口没有约束,数据迟早被污染。设计阶段一定要想清楚每个字段的"唯一生产者"是谁,并把校验逻辑放到仓储层,而不是等对象传到 UI 层才发现类型对不上。
数据模型设计这件事,我最大的体会是:它不像写一个新功能那样有即时成就感,更像是在给整栋楼浇筑地基。你花在实体划分、字段命名、迁移策略上的每一个小时,都会在后面某个功能验收时以"没有出问题"的形式悄悄还给你。如果你也正在做一个 Flutter for OpenHarmony 的应用,建议从第一个版本就把模型层当一等公民对待,实体拆分、仓储接口、版本迁移这些基本功做扎实了,后续的业务扩展才能真正顺畅起来。
