好的数据模型是应用的基石。本文从 E-Brufen 的 MoodType 枚举和 MoodEntry 模型出发完整拆解情绪数据的建模、序列化、持久化与测试总代码量不到 70 行却覆盖了数据层设计的全部核心要素。一、五种情绪的定义E-Brufen 将人类情绪简化为五种类型并为每种情绪赋予三个属性value数值、emoji表情符号、label中文标签// lib/models/mood_entry.dartenumMoodType{angry(1,,生气),sad(2,,难过),tired(3,,疲惫),calm(4,,平静),happy(5,,开心);finalint value;// 数值映射用于图表Y轴finalStringemoji;// UI展示finalStringlabel;// 中文标签constMoodType(this.value,this.emoji,this.label);staticMoodTypefromValue(int v)MoodType.values.firstWhere((m)m.valuev,orElse:()MoodType.calm,);}为什么 value 是 1-5 而非 0-4这不是随意设计的。五种情绪形成一个自然的情感梯度从愤怒(1)到开心(5)数值越大情绪越积极。这种数据即视觉映射的设计让 MoodChart 可以直接将情绪值用作柱状图的 Y 轴高度无需任何中间转换层。在mood_chart.dart中图表是这样利用 value 的// lib/widgets/mood_chart.dart —— 核心映射逻辑for(finalminweekMoods){finaldayIndexm.createdAt.weekday-1;dayCounts[dayIndex]?.add(m.moodType.value);// value 直接进入统计}// 计算日平均情绪值finaldayAvgsdayCounts.map((day,vals){if(vals.isEmpty)returnMapEntry(day,0.0);returnMapEntry(day,vals.reduce((a,b)ab)/vals.length);});// 根据平均值映射颜色Color_colorForAvg(double avg){if(avg4.5)returnColors.green;// 非常开心if(avg3.5)returnColors.lightGreen;// 较开心if(avg2.5)returnColors.amber;// 一般if(avg1.5)returnColors.orange;// 不太好if(avg0)returnColors.red;// 糟糕returnColors.grey.shade200;// 无记录}Insight给枚举的 value 赋 1-5 的递增整数并非随意。这使得情绪数据可以直接进行数值运算求平均、求和、排序5最积极、1最消极形成自然的排序。如果换用字符串枚举名如ANGRY不仅存储浪费还需要额外的映射表才能在图表中做数值比较。这种设计减少了至少一层转换 adapter。fromValue 的容错设计fromValue使用orElse: () MoodType.calm作为兜底。当 Hive 中意外读到非法值如版本升级导致的值域变化或数据损坏时不会抛出异常导致整页崩溃而是静默降级为calm平静。这是一种优雅降级的设计模式。单元测试也覆盖了这一场景// test/models/mood_entry_test.darttest(fromValue maps 1-5 correctly,(){expect(MoodType.fromValue(1),MoodType.angry);expect(MoodType.fromValue(2),MoodType.sad);expect(MoodType.fromValue(3),MoodType.tired);expect(MoodType.fromValue(4),MoodType.calm);expect(MoodType.fromValue(5),MoodType.happy);});test(fromValue falls back to calm for invalid values,(){expect(MoodType.fromValue(99),MoodType.calm);expect(MoodType.fromValue(-1),MoodType.calm);});枚举唯一性校验测试中还有一个容易被忽略但非常重要的检查——确保每种情绪的 value 不重复test(each mood type has unique value,(){finalvaluesMoodType.values.map((m)m.value).toSet();expect(values.length,MoodType.values.length);});如果将来有人给枚举新增一种情绪类型却不小心用了重复的 value比如都写成 3这个测试会立刻失败。这种低门槛但高价值的测试是防御性编程的典型案例。二、MoodEntry 数据模型// lib/models/mood_entry.dartclassMoodEntry{finalint?id;// 数据库自增IDnull表示未持久化finalMoodTypemoodType;// 情绪类型finalString?note;// 可选备注最长500字UI层通过TextField的maxLength:500限制finalDateTimecreatedAt;finalDateTimeupdatedAt;constMoodEntry({this.id,requiredthis.moodType,this.note,requiredthis.createdAt,requiredthis.updatedAt,});}设计要点逐一分析id 可空—— 未存入 Hive 前为 null存入后由 MoodStorage 自动分配自增 ID。这完美对应了尚未持久化与已持久化两种状态无需引入额外的状态标记字段。note 可空—— 用户可只记录情绪不写备注。在diary_page.dart中提交逻辑是note _noteController.text.isEmpty ? null : _noteController.text空字符串会被归一化为 null避免在 Hive 中存储无意义的空字符串。createdAt 与 updatedAt 分离—— 支持编辑追溯。初次创建时两者相同时刻编辑后 updatedAt 更新而 createdAt 不变。这在时间线页面按 created 排序但用户能看到最后编辑时间。const 构造函数—— 全字段 final所有实例不可变。这消除了数据竞态的隐患尤其在使用了 ChangeNotifier addListener 的响应式数据流中。三、copyWith不可变数据更新由于 MoodEntry 的所有字段都是 final编辑已有记录时必须创建全新实例。copyWith 模式是 Dart 中处理不可变对象的惯用方式MoodEntrycopyWith({int?id,MoodType?moodType,String?note,DateTime?createdAt,DateTime?updatedAt,})MoodEntry(id:id??this.id,moodType:moodType??this.moodType,note:note??this.note,createdAt:createdAt??this.createdAt,updatedAt:updatedAt??this.updatedAt,);实战用法—— 在diary_page.dart中编辑已有记录时copyWith 确保原有不变字段完整保留// lib/pages/diary/diary_page.dart —— _saveMood 中的编辑分支if(_editingId!null){finalexistingwidget.moodStorage.getAll().firstWhere((e)e.id_editingId,orElse:()MoodEntry(moodType:_selectedMood!,createdAt:timestamp,updatedAt:timestamp,),);widget.moodStorage.update(_editingId!,existing.copyWith(moodType:_selectedMood!,note:_noteController.text.isEmpty?null:_noteController.text,updatedAt:timestamp,));}注意这里的模式先通过 ID 查出原始记录existing再调用 copyWith 只覆盖变化的字段。id 和 createdAt 保持不变只有 moodType、note、updatedAt 三个字段被更新。四、手动 JSON 序列化E-Brufen 项目选择手动实现 toJson/fromJson而非依赖 json_serializable 代码生成。这在鸿蒙环境下有三个优势零代码生成依赖—— json_serializable 需要 build_runner 和 source_gen这些包在鸿蒙工具链上偶尔会有兼容性问题。字段名全可控—— 存储使用蛇形命名mood_type、created_at而 Dart 代码使用驼峰moodType、createdAt手动映射一目了然。5 个字段不值得引入生成器—— 代码生成器的配置代码比手写的序列化代码还长属于典型的过度工程化。MapString,dynamictoJson(){id:id,mood_type:moodType.value,// 存储数值而非枚举名 —— 便于图表直接读取note:note,created_at:createdAt.toIso8601String(),updated_at:updatedAt.toIso8601String(),};factoryMoodEntry.fromJson(MapString,dynamicjson)MoodEntry(id:json[id]asint?,moodType:MoodType.fromValue(json[mood_type]asint),note:json[note]asString?,createdAt:DateTime.parse(json[created_at]asString),updatedAt:DateTime.parse(json[updated_at]asString),);两个关键细节mood_type存储的是moodType.valueint而非枚举名如happy。这意味着读取时不需要枚举名到值的反向映射解析后的 int 可以直接用于图表统计少一层转换。DateTime 通过toIso8601String() / DateTime.parse()转字符串存储。Hive 的原生类型适配器不支持 DateTime 对象转为 ISO 8601 字符串是跨平台、跨版本最可靠的方案。五、Hive 持久化层jsonEncode 字符串格式MoodStorage 是完整的 Hive 数据访问层。它不依赖任何原生 SQLite 插件对鸿蒙平台完全兼容// lib/data/mood_storage.dart —— 核心 CRUDFutureintinsert(MoodEntryentry)async{finalid_nextId;// 自增 IDfinaldataentry.toJson();// 先转 Mapdata[id]id;// 将 ID 写回 Mapawait_box?.put(id,jsonEncode(data));// put 到 Hive boxnotifyListeners();// 通知 UI 刷新returnid;}存储流程MoodEntry → toJson() → Map → jsonEncode → String → Hive Box。Hive 中实际存储的内容是{id:1,mood_type:5,note:美好的一天,created_at:2026-07-06T10:30:00.000,updated_at:2026-07-06T10:30:00.000}读取流程Hive Box → String → jsonDecode → Map → _parse → MoodEntry。_parse是私有辅助方法复用了 fromJson 的逻辑MoodEntry_parse(int id,Stringraw){finalmapjsonDecode(raw)asMapString,dynamic;returnMoodEntry(id:map[id]asint?,moodType:MoodType.fromValue(map[mood_type]asint),note:map[note]asString?,createdAt:DateTime.parse(map[created_at]asString),updatedAt:DateTime.parse(map[updated_at]asString),);}为什么用 jsonEncode 字符串而不是 Hive TypeAdapterHive 提供了 TypeAdapter 机制直接存储对象但在鸿蒙 Flutter 环境中hive_generator的代码生成兼容性不够稳定。手动 jsonEncode 的方案虽然多了一次编解码开销但对于每天几十条记录的量级来说几乎无感知却换来了 100% 的跨平台确定性。响应式更新机制MoodStorage 继承ChangeNotifier每次 insert/update/delete 后调用notifyListeners()。DiaryPage 在 initState 中注册widget.moodStorage.addListener(_loadMoods)当数据变化时自动重新加载所有记录并更新 UI。六、模型单元测试将上述所有环节串联起来模型测试覆盖了枚举值映射、唯一性校验、序列化往返、copyWith 完整性// test/models/mood_entry_test.dart —— 完整测试套件importpackage:flutter_test/flutter_test.dart;importpackage:firstproject/models/mood_entry.dart;voidmain(){group(MoodType,(){test(fromValue maps 1-5 correctly,(){/* ... */});test(fromValue falls back to calm for invalid values,(){/* ... */});test(each mood type has unique value,(){/* ... */});});group(MoodEntry,(){test(toJson and fromJson round-trip,(){finalnowDateTime.now();finalentryMoodEntry(id:1,moodType:MoodType.happy,note:测试备注,createdAt:now,updatedAt:now,);finaljsonentry.toJson();finalrestoredMoodEntry.fromJson(json);expect(restored.id,1);expect(restored.moodType,MoodType.happy);expect(restored.note,测试备注);});test(copyWith updates specified fields,(){finalnowDateTime.now();finalentryMoodEntry(moodType:MoodType.calm,createdAt:now,updatedAt:now,);finalupdatedentry.copyWith(moodType:MoodType.sad,note:updated);expect(updated.moodType,MoodType.sad);expect(updated.note,updated);expect(updated.createdAt,now);// unchanged —— 关键断言});});}测试策略总结正向映射测试—— 确保 1-5 每个 value 正确映射到对应枚举5 个断言。异常值兜底测试—— 验证 99 和 -1 不会抛异常返回 calm默认可接受状态。唯一性约束测试—— 防止开发者在维护中引入重复的 value。序列化往返测试—— toJson 后 fromJson 能无损还原所有字段这是持久化的核心保证。copyWith 不变性测试—— 验证未传入的字段保持不变expect(updated.createdAt, now)这句至关重要。七、整体架构图┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ MoodPicker │ │ DiaryPage │ │ MoodChart │ │ (UI 组件) │────▶│ (状态管理) │────▶│ (图表组件) │ │ emoji label │ │ ChangeNotifier│ │ Y轴 value │ └──────────────┘ └──────┬───────┘ └──────────────┘ │ ┌────────▼───────┐ │ MoodStorage │ │ (Hive CRUD) │ │ jsonEncode/Dec │ └────────┬───────┘ │ ┌────────▼───────┐ │ Hive Box │ │ {id:1,...} │ └────────────────┘数据流用户点击 emoji → MoodPicker 回调 MoodType → DiaryPage 构建 MoodEntry → MoodStorage.insert 调用 toJson jsonEncode → 存入 Hive Box。反向MoodStorage.getAll → jsonDecode fromJson → List → DiaryPage setState 刷新 → MoodChart 读取 moodType.value 渲染柱状图。整条链路无第三方序列化库参与。小结从枚举到持久化E-Brufen 的模型层不到 70 行核心代码但覆盖了数据建模的全部要素类型安全—— MoodType 枚举约束了情绪的取值范围杜绝了非法情绪值数据即视觉—— value 1-5 直接映射图表 Y 轴减少转换层不可变性—— copyWith 模式保证每次更新都是新建实例无副作用手动序列化—— 5 个字段不值得引入代码生成器手动 toJson/fromJson 反而更清晰可控Hive 兼容—— jsonEncode 字符串绕过 TypeAdapter 依赖确保鸿蒙平台稳定性可测试性—— 5 个测试覆盖正向/异常/唯一性/往返/不变性全部场景这种够用就好、不过度设计的模型设计哲学贯穿整个 E-Brufen 项目。在跨平台尤其是鸿蒙场景下减少依赖就是减少风险——每少一个 build_runner 强依赖构建的可靠性就多一分。作者简介E-Brufen DevFlutter 鸿蒙开发者专注于跨平台移动应用开发与心理健康数字化。项目地址AtomGit - E-Brufen。
AtomGit Flutter 鸿蒙客户端:五种情绪模型设计
发布时间:2026/7/9 18:38:07
好的数据模型是应用的基石。本文从 E-Brufen 的 MoodType 枚举和 MoodEntry 模型出发完整拆解情绪数据的建模、序列化、持久化与测试总代码量不到 70 行却覆盖了数据层设计的全部核心要素。一、五种情绪的定义E-Brufen 将人类情绪简化为五种类型并为每种情绪赋予三个属性value数值、emoji表情符号、label中文标签// lib/models/mood_entry.dartenumMoodType{angry(1,,生气),sad(2,,难过),tired(3,,疲惫),calm(4,,平静),happy(5,,开心);finalint value;// 数值映射用于图表Y轴finalStringemoji;// UI展示finalStringlabel;// 中文标签constMoodType(this.value,this.emoji,this.label);staticMoodTypefromValue(int v)MoodType.values.firstWhere((m)m.valuev,orElse:()MoodType.calm,);}为什么 value 是 1-5 而非 0-4这不是随意设计的。五种情绪形成一个自然的情感梯度从愤怒(1)到开心(5)数值越大情绪越积极。这种数据即视觉映射的设计让 MoodChart 可以直接将情绪值用作柱状图的 Y 轴高度无需任何中间转换层。在mood_chart.dart中图表是这样利用 value 的// lib/widgets/mood_chart.dart —— 核心映射逻辑for(finalminweekMoods){finaldayIndexm.createdAt.weekday-1;dayCounts[dayIndex]?.add(m.moodType.value);// value 直接进入统计}// 计算日平均情绪值finaldayAvgsdayCounts.map((day,vals){if(vals.isEmpty)returnMapEntry(day,0.0);returnMapEntry(day,vals.reduce((a,b)ab)/vals.length);});// 根据平均值映射颜色Color_colorForAvg(double avg){if(avg4.5)returnColors.green;// 非常开心if(avg3.5)returnColors.lightGreen;// 较开心if(avg2.5)returnColors.amber;// 一般if(avg1.5)returnColors.orange;// 不太好if(avg0)returnColors.red;// 糟糕returnColors.grey.shade200;// 无记录}Insight给枚举的 value 赋 1-5 的递增整数并非随意。这使得情绪数据可以直接进行数值运算求平均、求和、排序5最积极、1最消极形成自然的排序。如果换用字符串枚举名如ANGRY不仅存储浪费还需要额外的映射表才能在图表中做数值比较。这种设计减少了至少一层转换 adapter。fromValue 的容错设计fromValue使用orElse: () MoodType.calm作为兜底。当 Hive 中意外读到非法值如版本升级导致的值域变化或数据损坏时不会抛出异常导致整页崩溃而是静默降级为calm平静。这是一种优雅降级的设计模式。单元测试也覆盖了这一场景// test/models/mood_entry_test.darttest(fromValue maps 1-5 correctly,(){expect(MoodType.fromValue(1),MoodType.angry);expect(MoodType.fromValue(2),MoodType.sad);expect(MoodType.fromValue(3),MoodType.tired);expect(MoodType.fromValue(4),MoodType.calm);expect(MoodType.fromValue(5),MoodType.happy);});test(fromValue falls back to calm for invalid values,(){expect(MoodType.fromValue(99),MoodType.calm);expect(MoodType.fromValue(-1),MoodType.calm);});枚举唯一性校验测试中还有一个容易被忽略但非常重要的检查——确保每种情绪的 value 不重复test(each mood type has unique value,(){finalvaluesMoodType.values.map((m)m.value).toSet();expect(values.length,MoodType.values.length);});如果将来有人给枚举新增一种情绪类型却不小心用了重复的 value比如都写成 3这个测试会立刻失败。这种低门槛但高价值的测试是防御性编程的典型案例。二、MoodEntry 数据模型// lib/models/mood_entry.dartclassMoodEntry{finalint?id;// 数据库自增IDnull表示未持久化finalMoodTypemoodType;// 情绪类型finalString?note;// 可选备注最长500字UI层通过TextField的maxLength:500限制finalDateTimecreatedAt;finalDateTimeupdatedAt;constMoodEntry({this.id,requiredthis.moodType,this.note,requiredthis.createdAt,requiredthis.updatedAt,});}设计要点逐一分析id 可空—— 未存入 Hive 前为 null存入后由 MoodStorage 自动分配自增 ID。这完美对应了尚未持久化与已持久化两种状态无需引入额外的状态标记字段。note 可空—— 用户可只记录情绪不写备注。在diary_page.dart中提交逻辑是note _noteController.text.isEmpty ? null : _noteController.text空字符串会被归一化为 null避免在 Hive 中存储无意义的空字符串。createdAt 与 updatedAt 分离—— 支持编辑追溯。初次创建时两者相同时刻编辑后 updatedAt 更新而 createdAt 不变。这在时间线页面按 created 排序但用户能看到最后编辑时间。const 构造函数—— 全字段 final所有实例不可变。这消除了数据竞态的隐患尤其在使用了 ChangeNotifier addListener 的响应式数据流中。三、copyWith不可变数据更新由于 MoodEntry 的所有字段都是 final编辑已有记录时必须创建全新实例。copyWith 模式是 Dart 中处理不可变对象的惯用方式MoodEntrycopyWith({int?id,MoodType?moodType,String?note,DateTime?createdAt,DateTime?updatedAt,})MoodEntry(id:id??this.id,moodType:moodType??this.moodType,note:note??this.note,createdAt:createdAt??this.createdAt,updatedAt:updatedAt??this.updatedAt,);实战用法—— 在diary_page.dart中编辑已有记录时copyWith 确保原有不变字段完整保留// lib/pages/diary/diary_page.dart —— _saveMood 中的编辑分支if(_editingId!null){finalexistingwidget.moodStorage.getAll().firstWhere((e)e.id_editingId,orElse:()MoodEntry(moodType:_selectedMood!,createdAt:timestamp,updatedAt:timestamp,),);widget.moodStorage.update(_editingId!,existing.copyWith(moodType:_selectedMood!,note:_noteController.text.isEmpty?null:_noteController.text,updatedAt:timestamp,));}注意这里的模式先通过 ID 查出原始记录existing再调用 copyWith 只覆盖变化的字段。id 和 createdAt 保持不变只有 moodType、note、updatedAt 三个字段被更新。四、手动 JSON 序列化E-Brufen 项目选择手动实现 toJson/fromJson而非依赖 json_serializable 代码生成。这在鸿蒙环境下有三个优势零代码生成依赖—— json_serializable 需要 build_runner 和 source_gen这些包在鸿蒙工具链上偶尔会有兼容性问题。字段名全可控—— 存储使用蛇形命名mood_type、created_at而 Dart 代码使用驼峰moodType、createdAt手动映射一目了然。5 个字段不值得引入生成器—— 代码生成器的配置代码比手写的序列化代码还长属于典型的过度工程化。MapString,dynamictoJson(){id:id,mood_type:moodType.value,// 存储数值而非枚举名 —— 便于图表直接读取note:note,created_at:createdAt.toIso8601String(),updated_at:updatedAt.toIso8601String(),};factoryMoodEntry.fromJson(MapString,dynamicjson)MoodEntry(id:json[id]asint?,moodType:MoodType.fromValue(json[mood_type]asint),note:json[note]asString?,createdAt:DateTime.parse(json[created_at]asString),updatedAt:DateTime.parse(json[updated_at]asString),);两个关键细节mood_type存储的是moodType.valueint而非枚举名如happy。这意味着读取时不需要枚举名到值的反向映射解析后的 int 可以直接用于图表统计少一层转换。DateTime 通过toIso8601String() / DateTime.parse()转字符串存储。Hive 的原生类型适配器不支持 DateTime 对象转为 ISO 8601 字符串是跨平台、跨版本最可靠的方案。五、Hive 持久化层jsonEncode 字符串格式MoodStorage 是完整的 Hive 数据访问层。它不依赖任何原生 SQLite 插件对鸿蒙平台完全兼容// lib/data/mood_storage.dart —— 核心 CRUDFutureintinsert(MoodEntryentry)async{finalid_nextId;// 自增 IDfinaldataentry.toJson();// 先转 Mapdata[id]id;// 将 ID 写回 Mapawait_box?.put(id,jsonEncode(data));// put 到 Hive boxnotifyListeners();// 通知 UI 刷新returnid;}存储流程MoodEntry → toJson() → Map → jsonEncode → String → Hive Box。Hive 中实际存储的内容是{id:1,mood_type:5,note:美好的一天,created_at:2026-07-06T10:30:00.000,updated_at:2026-07-06T10:30:00.000}读取流程Hive Box → String → jsonDecode → Map → _parse → MoodEntry。_parse是私有辅助方法复用了 fromJson 的逻辑MoodEntry_parse(int id,Stringraw){finalmapjsonDecode(raw)asMapString,dynamic;returnMoodEntry(id:map[id]asint?,moodType:MoodType.fromValue(map[mood_type]asint),note:map[note]asString?,createdAt:DateTime.parse(map[created_at]asString),updatedAt:DateTime.parse(map[updated_at]asString),);}为什么用 jsonEncode 字符串而不是 Hive TypeAdapterHive 提供了 TypeAdapter 机制直接存储对象但在鸿蒙 Flutter 环境中hive_generator的代码生成兼容性不够稳定。手动 jsonEncode 的方案虽然多了一次编解码开销但对于每天几十条记录的量级来说几乎无感知却换来了 100% 的跨平台确定性。响应式更新机制MoodStorage 继承ChangeNotifier每次 insert/update/delete 后调用notifyListeners()。DiaryPage 在 initState 中注册widget.moodStorage.addListener(_loadMoods)当数据变化时自动重新加载所有记录并更新 UI。六、模型单元测试将上述所有环节串联起来模型测试覆盖了枚举值映射、唯一性校验、序列化往返、copyWith 完整性// test/models/mood_entry_test.dart —— 完整测试套件importpackage:flutter_test/flutter_test.dart;importpackage:firstproject/models/mood_entry.dart;voidmain(){group(MoodType,(){test(fromValue maps 1-5 correctly,(){/* ... */});test(fromValue falls back to calm for invalid values,(){/* ... */});test(each mood type has unique value,(){/* ... */});});group(MoodEntry,(){test(toJson and fromJson round-trip,(){finalnowDateTime.now();finalentryMoodEntry(id:1,moodType:MoodType.happy,note:测试备注,createdAt:now,updatedAt:now,);finaljsonentry.toJson();finalrestoredMoodEntry.fromJson(json);expect(restored.id,1);expect(restored.moodType,MoodType.happy);expect(restored.note,测试备注);});test(copyWith updates specified fields,(){finalnowDateTime.now();finalentryMoodEntry(moodType:MoodType.calm,createdAt:now,updatedAt:now,);finalupdatedentry.copyWith(moodType:MoodType.sad,note:updated);expect(updated.moodType,MoodType.sad);expect(updated.note,updated);expect(updated.createdAt,now);// unchanged —— 关键断言});});}测试策略总结正向映射测试—— 确保 1-5 每个 value 正确映射到对应枚举5 个断言。异常值兜底测试—— 验证 99 和 -1 不会抛异常返回 calm默认可接受状态。唯一性约束测试—— 防止开发者在维护中引入重复的 value。序列化往返测试—— toJson 后 fromJson 能无损还原所有字段这是持久化的核心保证。copyWith 不变性测试—— 验证未传入的字段保持不变expect(updated.createdAt, now)这句至关重要。七、整体架构图┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ MoodPicker │ │ DiaryPage │ │ MoodChart │ │ (UI 组件) │────▶│ (状态管理) │────▶│ (图表组件) │ │ emoji label │ │ ChangeNotifier│ │ Y轴 value │ └──────────────┘ └──────┬───────┘ └──────────────┘ │ ┌────────▼───────┐ │ MoodStorage │ │ (Hive CRUD) │ │ jsonEncode/Dec │ └────────┬───────┘ │ ┌────────▼───────┐ │ Hive Box │ │ {id:1,...} │ └────────────────┘数据流用户点击 emoji → MoodPicker 回调 MoodType → DiaryPage 构建 MoodEntry → MoodStorage.insert 调用 toJson jsonEncode → 存入 Hive Box。反向MoodStorage.getAll → jsonDecode fromJson → List → DiaryPage setState 刷新 → MoodChart 读取 moodType.value 渲染柱状图。整条链路无第三方序列化库参与。小结从枚举到持久化E-Brufen 的模型层不到 70 行核心代码但覆盖了数据建模的全部要素类型安全—— MoodType 枚举约束了情绪的取值范围杜绝了非法情绪值数据即视觉—— value 1-5 直接映射图表 Y 轴减少转换层不可变性—— copyWith 模式保证每次更新都是新建实例无副作用手动序列化—— 5 个字段不值得引入代码生成器手动 toJson/fromJson 反而更清晰可控Hive 兼容—— jsonEncode 字符串绕过 TypeAdapter 依赖确保鸿蒙平台稳定性可测试性—— 5 个测试覆盖正向/异常/唯一性/往返/不变性全部场景这种够用就好、不过度设计的模型设计哲学贯穿整个 E-Brufen 项目。在跨平台尤其是鸿蒙场景下减少依赖就是减少风险——每少一个 build_runner 强依赖构建的可靠性就多一分。作者简介E-Brufen DevFlutter 鸿蒙开发者专注于跨平台移动应用开发与心理健康数字化。项目地址AtomGit - E-Brufen。