鸿蒙原生应用实战(二):数据层设计 — Model 定义与状态管理实战
·
鸿蒙原生应用实战(二):数据层设计 — Model 定义与状态管理实战
本文是系列第二篇,深入「心情日记」的数据层设计。一个优秀的应用离不开坚实的数据模型,本文将详细讲解 ArkTS 中的枚举定义、接口设计、AppStorage 全局状态管理,以及模拟数据的最佳实践。
一、数据层架构概览
在「心情日记」应用中,数据层承担着以下职责:
┌──────────────────────────┐
│ UI 页面层 │
│ Index / Write / Calendar│
│ Stats / Profile │
└────────────┬─────────────┘
│ 读写数据
┌────────────▼─────────────┐
│ AppStorage │ ← 全局状态存储
│ key: "entries" │
│ value: DiaryEntry[] │
└────────────┬─────────────┘
│ 封装调用
┌────────────▼─────────────┐
│ DiaryData.ets │ ← 数据模型层
│ - 枚举定义 │
│ - 接口定义 │
│ - 工具函数 │
│ - 模拟数据 │
└──────────────────────────┘
所有页面通过 DiaryData.ets 导出的类型和函数与数据层交互,不直接操作底层存储,实现了关注点分离。
二、枚举类型:MoodLevel(心情等级)
2.1 为什么用枚举?
在心情选择中,我们需要定义 9 种心情状态。枚举相比字符串常量有以下优势:
- 类型安全:编译期检查,避免拼写错误
- 代码提示:IDE 自动补全
- 可维护性:修改一处生效到处
2.2 枚举定义
export enum MoodLevel {
HAPPY = 'happy', // 开心
CALM = 'calm', // 平静
SAD = 'sad', // 难过
ANGRY = 'angry', // 生气
EXCITED = 'excited', // 兴奋
TIRED = 'tired', // 疲惫
ANXIOUS = 'anxious', // 焦虑
GRATEFUL = 'grateful',// 感恩
NEUTRAL = 'neutral' // 一般
}
设计考量:
- 使用字符串值而非数字值,便于序列化和调试
- 值使用英文小写,符合国际化扩展需求
- 每个枚举值都有明确的语义注释
三、接口定义:数据类型契约
3.1 DiaryEntry — 日记条目
export interface DiaryEntry {
id: string; // 唯一标识(时间戳+随机数)
date: string; // 日期 "2025-01-20" 格式
mood: MoodLevel; // 心情枚举
title: string; // 标题
content: string; // 正文内容
tags: string; // 标签 "工作,生活,旅行" 逗号分隔
}
字段设计说明:
| 字段 | 类型 | 为什么这么设计 |
|---|---|---|
id |
string |
使用 Date.now() + 随机数 生成,无需自增ID管理器 |
date |
string |
固定 YYYY-MM-DD 格式,方便字符串比较和排序 |
mood |
MoodLevel |
枚举类型,有限状态更可控 |
title |
string |
纯文本,足够满足日记场景 |
content |
string |
纯文本,避免富文本复杂度 |
tags |
string |
逗号分隔字符串,简单高效,无需额外的关联表 |
3.2 MoodInfo — 心情展示信息
export interface MoodInfo {
level: MoodLevel; // 对应枚举
icon: string; // Emoji 图标 "😊"
label: string; // 中文标签 "开心"
color: string; // 颜色值 "#FFD93D"
}
为什么把展示信息分离?
- 数据层(MoodLevel)与表现层(MoodInfo)解耦
- 方便后续国际化(切换 label 语言)
- 颜色、图标可单独调整而不影响业务逻辑
四、数据存储:9种心情配置
const ALL_MOODS: MoodInfo[] = [
{ level: MoodLevel.HAPPY, icon: '😊', label: '开心', color: '#FFD93D' },
{ level: MoodLevel.CALM, icon: '😌', label: '平静', color: '#6BCB77' },
{ level: MoodLevel.SAD, icon: '😢', label: '难过', color: '#4D96FF' },
{ level: MoodLevel.ANGRY, icon: '😠', label: '生气', color: '#FF6B6B' },
{ level: MoodLevel.EXCITED, icon: '🤩', label: '兴奋', color: '#FF6B9D' },
{ level: MoodLevel.TIRED, icon: '😴', label: '疲惫', color: '#A29BFE' },
{ level: MoodLevel.ANXIOUS, icon: '😰', label: '焦虑', color: '#FFA502' },
{ level: MoodLevel.GRATEFUL,icon: '🙏', label: '感恩', color: '#2ED573' },
{ level: MoodLevel.NEUTRAL, icon: '😐', label: '一般', color: '#95A5A6' }
];
颜色心理学:每种心情的颜色都经过精心选择:
- 开心 → 明黄色(温暖积极)
- 平静 → 翠绿色(宁静自然)
- 难过 → 蓝色(忧郁冷静)
- 生气 → 红色(热烈警示)
- 兴奋 → 粉红(活力四射)
- 疲惫 → 浅紫(柔和舒缓)
- 焦虑 → 橙色(不安警惕)
- 感恩 → 深绿(稳重感恩)
- 一般 → 灰色(中性低调)
五、工具函数设计
5.1 查询函数
// 获取所有心情列表(深拷贝避免外部修改)
export function getMoods(): MoodInfo[] {
let result: MoodInfo[] = [];
for (let i = 0; i < ALL_MOODS.length; i++) {
result.push(ALL_MOODS[i]);
}
return result;
}
// 根据 MoodLevel 查找对应的 MoodInfo
export function getMoodInfo(level: MoodLevel): MoodInfo {
for (let i = 0; i < ALL_MOODS.length; i++) {
if (ALL_MOODS[i].level === level) {
return ALL_MOODS[i];
}
}
return ALL_MOODS[8]; // 默认为 NEUTRAL
}
5.2 日期工具函数
// 生成唯一ID
export function generateId(): string {
return Date.now().toString() + Math.random().toString().slice(2, 8);
}
// 获取今天的日期字符串
export function getToday(): string {
let now = new Date();
let y = now.getFullYear();
let m = (now.getMonth() + 1).toString().padStart(2, '0');
let d = now.getDate().toString().padStart(2, '0');
return `${y}-${m}-${d}`;
}
// 格式化日期显示 "2025年01月20日"
export function getDateLabel(dateStr: string): string {
let parts: string[] = dateStr.split('-');
return `${parts[0]}年${parts[1]}月${parts[2]}日`;
}
// 获取短格式 "1月20日"
export function getTodayShort(): string {
let now = new Date();
return `${now.getMonth() + 1}月${now.getDate()}日`;
}
设计亮点:generateId() 使用毫秒时间戳 + 6位随机数,既保证了唯一性(同一毫秒内重复概率极低),又无需维护自增计数器。
六、AppStorage 全局状态管理深入讲解
6.1 什么是 AppStorage?
AppStorage 是鸿蒙 ArkTS 提供的全局应用状态管理机制,它有以下特点:
- 全局共享:所有页面组件均可访问
- 响应式:数据变化自动驱动 UI 更新
- 键值存储:类似 Map,通过 key 存取
- 应用级生命周期:应用启动时初始化,退出时销毁
6.2 基本用法
// 写入全局状态
AppStorage.set<DiaryEntry[]>('entries', entriesList);
// 读取全局状态
let stored = AppStorage.get<DiaryEntry[]>('entries');
// 删除
AppStorage.delete('entries');
6.3 在页面中使用
每个页面在 aboutToAppear 或 onPageShow 中读取数据:
aboutToAppear(): void {
this.loadData();
}
onPageShow(): void {
this.loadData(); // 每次页面显示都刷新
}
loadData(): void {
let stored = AppStorage.get<DiaryEntry[]>('entries');
this.entries = stored ? stored : [];
this.calcStats();
}
6.4 AppStorage vs LocalStorage vs PersistentStorage
| 特性 | AppStorage | LocalStorage | PersistentStorage |
|---|---|---|---|
| 作用域 | 全局应用 | 页面/组件 | 全局且持久化 |
| 响应式 | ✅ | ✅ | ✅ |
| 跨页面共享 | ✅ | ❌ | ✅ |
| 本地持久化 | ❌ | ❌ | ✅ |
| 生命周期 | 应用级别 | 页面级别 | 永久 |
为什么本项目选择 AppStorage?
- 日记数据需要在 5 个页面间共享
- 不需要持久化到磁盘(适合演示项目,正式项目建议使用 PersistentStorage 或数据库)
- 实现简单,适合中小型应用
七、模拟数据:让开发更高效
7.1 为什么需要模拟数据?
在新应用开发初期,没有真实用户数据。高质量的模拟数据能让你:
- 立即看到 UI 效果,而不是空页面
- 覆盖各种边界场景(有日记/无日记/不同心情)
- 展示数据变化趋势,方便调试统计功能
7.2 模拟数据实现
export function getSampleEntries(): DiaryEntry[] {
let entries: DiaryEntry[] = [
{ id: 'e1', date: '2025-01-20', mood: MoodLevel.HAPPY,
title: '发年终奖了',
content: '今天公司发了年终奖,比预期多不少!晚上请家人吃了顿大餐,很开心。',
tags: '工作,家庭' },
{ id: 'e2', date: '2025-01-21', mood: MoodLevel.CALM,
title: '周末看书',
content: '下午在咖啡馆看了一本好书,阳光洒在桌面上,感觉很平静充实。',
tags: '阅读,生活' },
{ id: 'e3', date: '2025-01-22', mood: MoodLevel.SAD,
title: '告别老朋友',
content: '最好的朋友搬家去另一个城市了,送别的时候有点难过。约好了常联系。',
tags: '友情' },
// ... 更多日记
];
return entries;
}
模拟数据设计的艺术:
- 涵盖全部 9 种心情
- 包含不同的标签组合
- 日期连续,便于测试签到统计
- 内容真实生动,让预览效果更自然
7.3 首次加载逻辑
loadData(): void {
let stored = AppStorage.get<DiaryEntry[]>('entries');
if (stored) {
this.entries = stored; // 有数据就加载已有数据
} else {
let samples = getSampleEntries();
this.entries = samples; // 没有数据则使用模拟数据
AppStorage.set<DiaryEntry[]>('entries', samples); // 存入全局状态
}
this.calcStats();
}
八、严格模式下的 ArkTS 编码规范
8.1 对象字面量显式类型
// ❌ 错误:arkts-no-untyped-obj-literals
let entry = { id: '1', date: '2025-01-20', mood: MoodLevel.HAPPY, title: 'test', content: '', tags: '' };
// ✅ 正确
let entry: DiaryEntry = { id: '1', date: '2025-01-20', mood: MoodLevel.HAPPY, title: 'test', content: '', tags: '' };
8.2 数组字面量类型推断
// ❌ 错误:arkts-no-noninferrable-arr-literals
let entries = [{ id: '1' }, { id: '2' }];
// ✅ 正确:提取为接口变量
let entries: DiaryEntry[] = [{ id: '1', date: '', mood: MoodLevel.NEUTRAL, title: '', content: '', tags: '' }];
8.3 ForEach 的 key 生成
// 使用稳定的唯一标识作为 key
ForEach(this.entries, (item: DiaryEntry) => {
ListItem() { /* ... */ }
}, (item: DiaryEntry) => item.id) // 使用 item.id 作为 key
最佳实践:
key生成函数必须返回稳定且唯一的值- 使用
item.id而非index - 避免使用
Math.random()作为 key(会导致不必要的重渲染)
九、数据流完整闭环
理解了所有数据层组件后,让我们看一个完整的数据流动周期:
写日记流程:
1. 用户在 WritePage 填写内容
2. 点击"保存日记"按钮
3. saveEntry() 创建 DiaryEntry 对象
4. 通过 AppStorage.set() 存储
5. router.back() 返回到首页
6. 首页 onPageShow() 触发重新加载
7. 新日记出现在最近记录列表中
删除日记流程:
1. 用户在 CalendarPage 查看日记详情
2. 点击"删除"按钮
3. deleteEntry() 移除对应条目
4. 通过 AppStorage.set() 更新存储
5. buildCalendar() 重新渲染日历
6. 日记从日历和列表中消失
十、下篇预告
数据层已经准备就绪!在下一篇UI构建实战中,我们将:
- 打造首页的今日心情卡片
- 实现 4 个快捷操作按钮
- 构建最近日记列表
- 开发写日记页的心情选择器
- 掌握 @Builder 装饰器复用 UI
敬请期待!
如果你对数据层设计有更好的建议,欢迎留言讨论!
更多推荐

所有评论(0)