鸿蒙原生应用实战(二):数据层设计 — 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 在页面中使用

每个页面在 aboutToAppearonPageShow 中读取数据:

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 为什么需要模拟数据?

在新应用开发初期,没有真实用户数据。高质量的模拟数据能让你:

  1. 立即看到 UI 效果,而不是空页面
  2. 覆盖各种边界场景(有日记/无日记/不同心情)
  3. 展示数据变化趋势,方便调试统计功能

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

敬请期待!
在这里插入图片描述


如果你对数据层设计有更好的建议,欢迎留言讨论!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐