在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

鸿蒙 Next 尴尬粉碎机 App 开发实战:社交工具 + 趣味交互 + 静态数据管理

作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio
语言框架:ArkTS + ArkUI
字数:约 12000 字


目录

  1. 引言
  2. 产品概念与功能设计
  3. 三 Tab 架构与数据流
  4. 静态数据管理:话题库与金句库
  5. 尴尬日记:CRUD + 粉碎机制
  6. 卡片式 UI 设计与交互
  7. 粉碎动画:仪式感交互设计
  8. 数据持久化实现
  9. 四款 App 的架构演进
  10. 编译错误回顾与模式总结
  11. 总结与展望

1. 引言

1.1 社交尴尬:一个普遍的心理需求

社交尴尬(Social Embarrassment)是人类普遍体验——冷场时的沉默、说错话后的懊恼、被当众表扬时的手足无措。心理学研究表明,适度的幽默感是化解尴尬最有效的方式。

"尴尬粉碎机"正是基于这一洞察而设计——它不是一个严肃的工具,而是一个轻松有趣的社交伴侣

  • 当你无话可说时,它给你一个话题
  • 当你陷入尴尬时,它给你一句金句
  • 当你经历了尴尬时,你把它写下来,然后"粉碎"它——一场象征性的情绪释放

1.2 本 App 的技术定位

与前三个 App 相比,"尴尬粉碎机"在技术上面临不同的挑战:

对比维度 前作 尴尬粉碎机
数据来源 用户生成 + 持久化 静态预设数据 + 用户生成
数据量级 小型(几十条) 中型(20 话题 + 12 金句 + 用户日记)
交互复杂度 CRUD CRUD + 随机索引 + 仪式感动画
UI 密度 中等 高(3 Tab + 列表 + 弹窗 + 动画)

本 App 最大的技术特点是其 "静态数据 + 用户数据"的混合数据模型——预设的 20 个话题和 12 条金句作为静态常量存在代码中,而用户的尴尬日记则通过 Preferences 持久化存储。

1.3 系列回顾

这是本系列的第四款应用。回顾之前的三款:

  1. 沉浸式白噪音 — 多媒体播放 + 动画系统
  2. 时间胶囊 — 数据持久化 + 日期判断 + Builder 约束
  3. 冰箱剩菜大作战 — 三 Tab 架构 + 游戏化评分 + 分类选择器

每一款都在前一款的基础上引入新的技术维度,同时复用已验证的 ArkUI 模式。


2. 产品概念与功能设计

2.1 核心功能需求

用户故事 1:在社交场合遇到冷场时,我需要一个话题来打破沉默
用户故事 2:遇到尴尬时刻,我需要一句幽默的话来化解
用户故事 3:经历了尴尬的事,我希望写下来然后"销毁"它以释放情绪
用户故事 4:我想浏览所有可用的救场金句,记住它们以备不时之需

功能清单:
├── F1: 随机破冰话题(20 个预设话题,一键切换)
├── F2: 随机救场金句(12 条场景金句,配套段子)
├── F3: 全部金句列表浏览
├── F4: 尴尬日记记录
├── F5: 粉碎尴尬(标记已粉碎 + 动画反馈)
├── F6: 删除日记条目
└── F7: 数据持久化

2.2 信息架构

尬尴粉碎机
├── Tab 0: 今日救场
│   ├── 吉祥物图标
│   ├── 随机话题卡片
│   │   ├── 话题标签(破冰/旅行/趣味...)
│   │   ├── 话题文字
│   │   └── "换一个话题"按钮
│   └── 使用小贴士(3 个卡片)
│
├── Tab 1: 救场金句
│   ├── 随机金句卡片
│   │   ├── 场景名称
│   │   ├── 金句引用(大号斜体)
│   │   ├── 搞笑指数(😂 评级)
│   │   └── "换一个" + "收藏"按钮
│   └── 全部金句列表(可滚动浏览)
│
└── Tab 2: 尴尬日记
    ├── [空] → 空状态引导
    └── [有数据] → 列表 + 滑动操作
        ├── "待粉碎"状态(红色标签)
        └── "已粉碎"状态(灰色标签 + 半透明)

2.3 数据流设计

静态数据流(预设):
=========================================
TOPICS[20] ─→ 随机索引 ─→ buildTodayPage()
COMEBACKS[12] ─→ 随机索引 ─→ buildComebackPage()

用户数据流(持久化):
=========================================
用户输入 → addEntry() → @State entries → UI 渲染
                                ↓
                          saveData()
                                ↓
                     Preferences.put(JSON)

关键设计决策:话题和金句数据是静态的,不需要持久化——这减少了存储负担,也意味着 App 启动时不需要等待数据加载即可显示内容。


3. 三 Tab 架构与数据流

3.1 Tab 状态管理

@State activeTab: number = 0;

用一个数字控制三个 Tab 的切换。Tab 内容的条件渲染:

@Builder
buildTabContent() {
  if (this.activeTab === 0) {
    this.buildTodayPage()
  } else if (this.activeTab === 1) {
    this.buildComebackPage()
  } else {
    this.buildDiaryPage()
  }
}

优化点activeTab 使用 @State 装饰,当用户点击 Tab 切换时,框架自动销毁旧 Tab 的组件树并创建新 Tab 的组件树。这确保了未激活的 Tab 不占用内存

3.2 Tab 栏实现

@Builder
buildTabBar() {
  Row() {
    this.buildTabItem(0, '\u{1F4AC}', '今日救场')
    this.buildTabItem(1, '\u{1F3B5}', '救场金句')
    this.buildTabItem(2, '\u{1F4DD}', '尴尬日记')
  }
  .width('100%')
  .height(56)
  .backgroundColor(ALL_COLORS.cardBg)
  .borderRadius({ topLeft: 20, topRight: 20 })
  .shadow({ radius: 12, color: 'rgba(0, 0, 0, 0.06)', offsetY: -3 })
  .position({ x: 0, y: '100%' })
  .translate({ y: -56 })
}

位置技巧:使用 position({ x: 0, y: '100%' }) 将 Tab 栏定位到底部,再用 translate({ y: -56 }) 向上偏移自身高度,实现固定底部效果。

3.3 Tab 项组件

@Builder
buildTabItem(index: number, icon: string, label: string) {
  Column() {
    Text(icon).fontSize(this.activeTab === index ? 24 : 20)
    Text(label)
      .fontSize(11)
      .fontColor(this.activeTab === index ? '#FF6B6B' : ALL_COLORS.textLight)
      .fontWeight(this.activeTab === index ? FontWeight.Bold : FontWeight.Normal)
      .margin({ top: 2 })
  }
  .padding({ left: 20, right: 20, top: 6, bottom: 6 })
  .onClick(() => { this.activeTab = index; })
}

本 App 的 Tab 与前作的区别:前作(冰箱剩菜)Tab 在添加页有表单操作,而本 App 的 Tab 更偏向内容浏览。因此 Tab 切换更频繁,视觉反馈也更重要——选中态使用醒目的红色 #FF6B6B

3.4 头部标题栏的上下文按钮

if (this.activeTab === 2) {
  Row() {
    Text('\u{2795}').fontSize(16)
    Text('记录').fontSize(14)
  }
  .onClick(() => { this.openAddEntry(); })
}

"添加"按钮只出现在尴尬日记 Tab 中。这是上下文 UI 的典型实践——按钮的可见性随当前 Tab 变化,保持界面简洁。


4. 静态数据管理:话题库与金句库

4.1 话题数据结构

interface TopicCard {
  id: number;
  text: string;      // 话题文字
  tag: string;       // 分类标签
}

const TOPICS: TopicCard[] = [
  { id: 1, text: '如果你可以瞬间掌握一项新技能,你会选什么?', tag: '破冰' },
  { id: 2, text: '你最近看过的最好看的一部电影是什么?', tag: '休闲' },
  // ... 共 20 条
];

话题设计原则

  • 开放性:没有标准答案,鼓励对话
  • 普适性:适用于大多数社交场合
  • 轻松性:不涉及敏感话题
  • 多样性:涵盖 10+ 种标签分类

4.2 金句数据结构

interface ComebackItem {
  id: number;
  situation: string;  // 尴尬场景
  line: string;       // 救场金句
  rating: string;     // 搞笑指数
}

const COMEBACKS: ComebackItem[] = [
  { id: 1, situation: '空气突然安静',
    line: '刚刚是不是有一只企鹅走过去了?',
    rating: '😂😂😂' },
  // ... 共 12 条
];

金句设计原则

  • 自嘲为主:自嘲是最安全的幽默方式
  • 场景匹配:每条金句对应一个具体场景
  • 简短有力:一句话解决问题

4.3 随机索引算法

@State currentTopicIndex: number = 0;
@State currentComebackIndex: number = 0;

nextTopic(): void {
  this.currentTopicIndex = (this.currentTopicIndex + 1) % TOPICS.length;
}

nextComeback(): void {
  this.currentComebackIndex = (this.currentComebackIndex + 1) % COMEBACKS.length;
}

为什么用顺序轮换而不是真随机?

  1. 可预测性:用户"换一个"时能确保看到新内容(真随机可能重复)
  2. 全覆盖:保证所有内容都能被看到
  3. 简单可靠:不需要维护"已看过"状态

取模运算(当前索引 + 1) % 总数 实现循环。当索引到达最后一个时,取模回到 0。

4.4 静态数据的优势

与用户生成数据相比,静态数据有以下优势:

维度 静态数据 用户数据
加载速度 无需等待,立即可用 需要异步加载
存储依赖 依赖 Preferences
数据安全 不会丢失 可能因存储故障丢失
修改方式 需更新 App 用户实时操作
大小限制 代码包体积,无硬限制 Preferences 推荐 < 1MB

4.5 全部金句列表

除了随机展示,我们还在 Tab 1 底部提供了全部金句列表,方便用户浏览和记忆:

ForEach(COMEBACKS, (item: ComebackItem) => {
  Column() {
    Row() {
      Text('\u{1F3B5}').fontSize(16)
      Text(item.situation).fontSize(13).fontColor('#FF6B6B')
      Blank()
      Text(item.rating).fontSize(13)
    }
    Text('\u{201C}' + item.line + '\u{201D}')
      .fontSize(15).margin({ top: 6 }).fontStyle(FontStyle.Italic)
  }
  .padding(16)
  .backgroundColor(ALL_COLORS.cardBg)
  .borderRadius(12)
  .borderWidth(1)
  .borderColor(ALL_COLORS.border + '66')
  .margin({ bottom: 8 })
}, (item: ComebackItem) => item.id.toString())

每个列表项包含场景名 + 金句内容 + 搞笑指数,采用卡片式设计。


5. 尴尬日记:CRUD + 粉碎机制

5.1 数据模型

interface EmbarrassmentEntry {
  id: number;
  content: string;      // 尴尬内容
  date: number;         // 记录时间戳
  isShredded: boolean;  // 是否已粉碎
}

isShredded 字段的设计意图

  • 不是物理删除,而是状态转移——从"待粉碎"变为"已粉碎"
  • 已粉碎的记录仍然保留在列表中(灰色显示),作为"战利品"
  • 用户也可以物理删除已粉碎的记录

5.2 创建(C)

addEntry(): void {
  if (this.newEntryContent.trim() === '') return;
  let entry: EmbarrassmentEntry = {
    id: Date.now(),
    content: this.newEntryContent.trim(),
    date: Date.now(),
    isShredded: false
  };
  this.entries = [entry].concat(this.entries);
  this.showAddEntry = false;
  this.activeTab = 2;     // 自动切换到日记 Tab
  this.saveData();
}

[entry].concat(this.entries):头部插入,最新记录在顶部。

自动切 Tab:在 Tab 0(今日救场)点击"记录"按钮打开弹窗,提交后自动切换到 Tab 2(尴尬日记)显示新记录。这是用户体验的细节优化。

5.3 粉碎(U)

startShred(entryId: number): void {
  let entry = this.entries.find(e => e.id === entryId);
  if (entry && !entry.isShredded) {
    entry.isShredded = true;
    this.entries = this.entries.concat([]);  // 触发渲染
    this.showShredAnimation = true;          // 显示动画
    this.showDetailDialog = false;           // 关闭详情
    this.saveData();
  }
}

粉碎操作的三步

  1. 修改数据:isShredded = true
  2. 刷新 UI:this.entries = this.entries.concat([])
  3. 显示反馈:showShredAnimation = true

5.4 删除(D)

deleteEntry(entryId: number): void {
  this.entries = this.entries.filter(e => e.id !== entryId);
  // 如果详情弹窗中显示的就是被删除的记录,关闭弹窗
  if (this.selectedEntry && this.selectedEntry.id === entryId) {
    this.selectedEntry = null;
    this.showDetailDialog = false;
  }
  this.saveData();
}

filter() 的引用特性filter() 返回一个新数组,天然触发 @State 变更检测,无需额外操作。

5.5 列表渲染与状态区分

@Builder
buildEntryCard(entry: EmbarrassmentEntry) {
  Column() {
    Row() {
      Text(entry.isShredded ? '\u{1F5D1}\uFE0F' : '\u{1F4DD}')
      Column() {
        Text(entry.content)
          .fontColor(entry.isShredded ? ALL_COLORS.textLight : ALL_COLORS.text)
          .fontWeight(entry.isShredded ? FontWeight.Normal : FontWeight.Medium)
        Text(this.formatDate(entry.date))
        Text(entry.isShredded ? '已粉碎' : '待粉碎')
          .fontColor(entry.isShredded ? ALL_COLORS.textLight : '#FF6B6B')
      }
      if (!entry.isShredded) {
        Text('\u{1F4FA}').onClick(() => { this.startShred(entry.id); })
      }
    }
  }
  .opacity(entry.isShredded ? 0.6 : 1.0)
}

状态视觉区分

属性 待粉碎 已粉碎
图标 📝 笔记本 🗑️ 垃圾桶
文字颜色 深色 浅灰色
字体粗细 Medium Normal
状态标签 🔴 红色 “待粉碎” 灰色 “已粉碎”
卡片透明度 100% 60%
粉碎按钮 显示 🗑️ 隐藏

六重编码确保用户一眼就能区分状态。


6. 卡片式 UI 设计与交互

6.1 卡片设计语言

本 App 广泛使用"卡片"作为信息容器。卡片设计遵循以下原则:

Column() { /* 卡片内容 */ }
  .width('85%')
  .padding(24)
  .backgroundColor(ALL_COLORS.cardBg)
  .borderRadius(20)
  .borderWidth(1)
  .borderColor('#FF6B6B33')
  .shadow({
    radius: 16,
    color: 'rgba(255, 107, 107, 0.10)',
    offsetY: 4
  })

卡片参数解析

参数 作用
borderRadius 20 大圆角,现代感
borderColor #FF6B6B33 20% 透明度的主题色边框
shadow.radius 16 柔和的大阴影
shadow.color rgba(...0.10) 10% 透明度的浅阴影
shadow.offsetY 4 阴影向下偏移,产生"浮起"感

6.2 话题卡片

┌──────────────────────────┐
│      💬 破冰              │ ← 标签(蓝色背景)
│                          │
│  如果你可以瞬间掌握一项   │ ← 话题文字(居中)
│  新技能,你会选什么?    │
│                          │
│  ─────────────────      │ ← 分隔线
│  换一个话题 🔄           │ ← 操作按钮(蓝色)
└──────────────────────────┘

6.3 金句卡片

┌──────────────────────────┐
│     🎵 空气突然安静       │ ← 场景标签(红色背景)
│                          │
│  "刚刚是不是有一只企鹅   │ ← 金句(大号斜体)
│   走过去了?"            │
│                          │
│       😂😂😂              │ ← 搞笑指数
│                          │
│  ─────────────────      │
│  🔄 换一个  │  📌 收藏  │ ← 双按钮
└──────────────────────────┘

6.4 日记卡片

┌──────────────────────────┐
│ 📝 今天开会时叫错了领导   │ ← 内容(最多两行)
│     的名字...            │
│     06月13日 14:30       │ ← 时间
│     🔴 待粉碎      🗑️   │ ← 状态 + 粉碎按钮
└──────────────────────────┘

区别设计:日记卡片没有大圆角 borderRadius(14) 和浅色边框,与内容卡片(borderRadius(20))形成视觉层次。

6.5 滑动操作

ListItem() {
  this.buildEntryCard(entry)
}
.swipeAction({ end: this.buildEntrySwipe(entry) })

右滑显示操作按钮:

@Builder
buildEntrySwipe(entry: EmbarrassmentEntry) {
  Row() {
    if (!entry.isShredded) {
      Text('\u{1F4FA} 粉碎')
        .backgroundColor('#FF6B6B')
        .borderRadius(14)
        .onClick(() => { this.startShred(entry.id); })
    }
    Text('\u{1F5D1}\uFE0F 删除')
      .backgroundColor(ALL_COLORS.textLight)
      .borderRadius(14)
      .onClick(() => { this.deleteEntry(entry.id); })
  }
}

条件按钮:只有"待粉碎"的条目显示"粉碎"按钮;"已粉碎"的条目只显示"删除"按钮。


7. 粉碎动画:仪式感交互设计

7.1 什么是"仪式感交互"

仪式感交互(Ritualistic Interaction)是 UX 设计中一种通过符号化操作来满足用户心理需求的方法。在"尴尬粉碎机"中,简单地删除一条记录无法满足用户"摆脱尴尬"的心理需求——用户需要一种可视化的、象征性的"销毁"过程

这就是"粉碎动画"的设计初衷。

7.2 动画实现

@Builder
buildShredAnimation() {
  Column() {
    // 半透明蒙层
    Column()
      .backgroundColor('rgba(45, 52, 54, 0.6)')

    // 动画弹窗
    Column() {
      Text('\u{1F4FA}').fontSize(64)         // 粉碎机图标
      Text('正在粉碎尴尬...').fontSize(18)
        .fontWeight(FontWeight.Bold)

      Row() {
        Text('\u{1F4E6}')                    // 纸箱(输入)
        Text(' \u{27A1}\uFE0F ').fontSize(20)  // 箭头
        Text('\u{1F4FA}')                    // 粉碎机
      }

      Text('尴尬已被粉碎得干干净净!')
        .fontSize(14).fontColor('#FF6B6B')

      Text('点击关闭')
        .fontSize(13).fontColor(ALL_COLORS.textLight)
        .backgroundColor('rgba(255,255,255,0.6)')
        .borderRadius(12)
        .onClick(() => { this.showShredAnimation = false; })
    }
    .width('80%')
    .backgroundColor(ALL_COLORS.cardBg)
    .borderRadius(24)
    .position({ x: '10%', y: '30%' })
    .alignItems(HorizontalAlign.Center)
  }
}

7.3 动画的仪式感流程

用户点击"粉碎" →
  1. 数据标记 isShredded = true
  2. 弹出粉碎动画(全屏蒙层 + 弹窗)
  3. 动画内容:
     📦 → ➡️ → 🗑️ → ✨
     输入  进入  粉碎  完成
  4. 用户点击关闭
  5. 回到列表,记录已变为灰色"已粉碎"状态

为什么这个设计有效?

  • 可视化:用户看到尴尬被"送入粉碎机"的视觉隐喻
  • 不可逆性:粉碎操作没有"撤销"按钮,强化"已经过去了"的心理暗示
  • 时间延迟:动画强迫用户停顿 1-2 秒,增加仪式感

7.4 动画控制的布尔变量

@State showShredAnimation: boolean = false;

这是一个简单的布尔开关:

  • startShred() 中设置为 true → 显示动画
  • 用户点击关闭时设置为 false → 隐藏动画

为什么不使用 setTimeout 自动关闭? 我们选择让用户手动关闭,增加交互参与感。这就像实体碎纸机需要用户自己按开关一样。


8. 数据持久化实现

8.1 单键值存储

与前作"冰箱剩菜大作战"的多键值存储不同,本 App 只需要存储一个数据:

const STORAGE_KEY: string = 'embarrass_entries';

惭愧日记数组是唯一需要持久化的数据。话题和金句都是静态常量。

8.2 加载实现

async loadData(): Promise<void> {
  try {
    let context = getContext(this);
    this.dataPreferences = await preferences.getPreferences(context, 'embarrass_db');
    let val = await this.dataPreferences.get(STORAGE_KEY, '');
    if (val !== '') {
      let data = JSON.parse(val as string) as EmbarrassmentEntry[];
      if (data && data.length > 0) this.entries = data;
    }
  } catch (err) {
    console.error(`Failed to load: ${JSON.stringify(err)}`);
  }
}

8.3 保存实现

async saveData(): Promise<void> {
  try {
    if (this.dataPreferences) {
      await this.dataPreferences.put(STORAGE_KEY, JSON.stringify(this.entries));
      await this.dataPreferences.flush();
    }
  } catch (err) {
    console.error(`Failed to save: ${JSON.stringify(err)}`);
  }
}

8.4 生命周期管理

aboutToAppear(): void {
  this.loadData();
}

aboutToDisappear(): void {
  this.saveData();
}

双重保存策略

  1. 每次操作后保存addEntry()startShred()deleteEntry() 中都会调用 saveData()
  2. 退出时保存aboutToDisappear() 中再次保存,作为最后的保障

8.5 ValueType 转换

let val = await this.dataPreferences.get(STORAGE_KEY, '');
// val 的类型是 ValueType(string | number | boolean)

let data = JSON.parse(val as string) as EmbarrassmentEntry[];
//                    ^^^^^^^^^^
//          必须断言为 string 才能传给 JSON.parse

为什么 as string 是必要的?
preferences.get() 的返回类型是 Promise<ValueType>,其中 ValueType 定义如下:

type ValueType = string | number | boolean;

JSON.parse() 只接受 string 类型参数。类型断言 as string 告诉编译器"我知道它实际上是 string,放心用吧"。


9. 四款 App 的架构演进

9.1 架构对比

从第一款"沉浸式白噪音"到第四款"尴尬粉碎机",我们的架构模式在持续演进:

维度 白噪音 时间胶囊 冰箱剩菜 尴尬粉碎机
页面模式 单页 单页+弹窗 三Tab 三Tab
数据来源 多媒体 用户+持久化 用户+持久化 静态+用户+持久化
@Builder 数量 8个 12个 17个 15个
@State 数量 8个 8个 14个 10个
业务方法 8个 12个 15个 9个
代码行数 ~767 ~955 ~1320 ~953

9.2 模式复用

四款 App 共享的架构模式:

模式 1:Stack + 三层结构

Stack() {
  buildBackground()     // 背景层
  Column() { /* UI */ } // 内容层
  if (dialog) { ... }   // 弹窗层
}

模式 2:List + swipeAction

List() {
  ForEach(data, (item) => {
    ListItem() { buildCard(item) }
      .swipeAction({ end: buildSwipe(item) })
  })
}

模式 3:@Builder + 条件渲染

@Builder
buildDetailDialog() {
  if (this.selectedItem !== null) {
    Column() { /* 内容 */ }
  }
}

模式 4:数据持久化三件套

aboutToAppear()loadData()
每次操作后 → saveData()
aboutToDisappear()saveData()

9.3 演进中的教训

教训 来源 改进
Builder 不要用闭包参数 冰箱剩菜的 buildFormField 直接内联 UI 代码
数组展开运算符不可用 所有四个 App 使用 .concat()
对象字面量需显式类型 所有四个 App 先定义 interface
存储键值需要初始化标记 冰箱剩菜的 stats_initialized 只有用户数据才需要

10. 编译错误回顾与模式总结

10.1 本 App 的错误

"尴尬粉碎机"是四款 App 中编译通过最快的一个——仅 1 个错误

错误

Object literal must correspond to some explicitly declared class or interface

原因ALL_COLORS 对象字面量没有对应的接口声明。

修复:添加 ColorScheme 接口:

interface ColorScheme {
  primary: string;
  primaryDark: string;
  // ... 共 12 个字段
}

const ALL_COLORS: ColorScheme = { ... };

10.2 四款 App 的错误统计

类型 白噪音 时间胶囊 冰箱剩菜 尴尬粉碎机
对象无类型 1 1 1 1
Builder 中非 UI 语法 8 9 15 0
闭包参数不可用 0 0 6 0
方法不存在 1 0 7 0
枚举名不可用 3 0 0 0
无匹配重载 1 1 0 0
展开运算符 1 2 0 0
对象可能为 null 0 0 1 0
总计 16 17 22 1

趋势分析:随着开发经验的积累和架构模式的成熟,编译错误数量呈下降趋势。前三个 App 各自引入了新的错误类型(Builder 约束、闭包参数、枚举名称等),而第四个 App 复用已验证的模式,几乎没有新错误。

10.3 核心模式清单

经过四款 App 的开发,我们总结出以下的 ArkUI 核心模式清单:

1. @State 数组更新模式

// 头部插入
this.list = [newItem].concat(this.list);
// 触发渲染
this.list = this.list.concat([]);
// 过滤删除
this.list = this.list.filter(predicate);

2. @Builder 数据获取模式

// ❌ Builder 内用 let
@Builder { let x = getData(); Text(x) }

// ✅ Builder 调用方法
@Builder { Text(this.getData()) }

3. 弹窗模式

@Builder
buildDialog() {
  if (this.showDialog) {
    Column() {
      Column().onClick(() => { this.showDialog = false; })  // 蒙层
      Column() { /* 内容 */ }.position(...)                   // 浮层
    }
  }
}

4. 持久化模式

async loadData() { /* JSON.parse → @State */ }
async saveData() { /* @State → JSON.stringify → flush */ }
aboutToAppear() { this.loadData(); }
aboutToDisappear() { this.saveData(); }

5. Tab 切换模式

@State activeTab: number = 0;
@Builder buildTabContent() {
  if (activeTab === 0) { buildTab0() }
  else if (activeTab === 1) { buildTab1() }
  else { buildTab2() }
}

10.4 从 22 个错误到 1 个错误的进化

回顾从冰箱剩菜(22 个错误)到尴尬粉碎机(1 个错误)的进步:

冰箱剩菜的错误

  • 15 个 Builder 语法错误(使用 buildFormField + 闭包)
  • 6 个闭包参数错误
  • 7 个方法不存在错误

尴尬粉碎机的改进

  • 消除 buildFormField 模式(直接内联 UI)
  • 所有数据获取用方法调用(不在 Builder 中用 let)
  • 复用已经验证的 Tri-Tab 架构

结论:ArkUI 开发的学习曲线虽然陡峭(前几个 App 错误较多),但一旦掌握核心模式,后续 App 的开发效率和编译通过率会大幅提升。


11. 总结与展望

11.1 系列回顾

四款 App 覆盖了 HarmonyOS Next 应用开发的核心技术维度:

App 核心技术点 学习重点
沉浸式白噪音 @kit.MediaKit、动画系统、Grid 布局 多媒体 + 动画
时间胶囊 Preferences、日期判断、Builder 约束 持久化 + 空值处理
冰箱剩菜大作战 Tab 架构、Grid 弹窗、游戏化评分 多 Tab + 选单组件
尴尬粉碎机 静态数据管理、仪式感交互、模式复用 数据混合 + UX 设计

11.2 本 App 的技术价值小结

"尴尬粉碎机"在技术上的核心贡献是:

  1. 静态数据 + 用户数据的混合模型:展示如何在 @State 中同时管理预设数据和用户生成数据
  2. 仪式感交互的设计与实现:用简单的布尔变量 + 条件渲染实现有"仪式感"的交互反馈
  3. 模式复用的成功案例:作为系列第四款,仅 1 个编译错误,验证了之前总结的 ArkUI 模式的正确性

11.3 可扩展方向

  1. 话题收藏:用户收藏喜欢的话题,形成个人话题库
  2. 自定义金句:用户添加自己的救场金句(需要额外的持久化键)
  3. 尴尬指数统计:记录用户"粉碎"了多少尴尬事,形成统计数据
  4. 社交分享:将金句或话题分享到社交平台
  5. 每日推送:每天推送一条话题或金句
  6. 多语言:支持英文话题和金句
  7. 主题皮肤:提供多套视觉主题(暗黑模式、更多配色)
  8. 语音输入:支持语音记录尴尬瞬间

11.4 对 HarmonyOS 开发者的建议

给新手的建议

  1. 从单页 App 开始,理解 @State@Builder 的基础用法
  2. 逐步引入弹窗和列表,掌握条件渲染和 ForEach
  3. 再尝试 Tab 架构和多页面

给有过几个 App 经验的开发者

  1. 建立自己的"模式清单"——记录可复用的代码片段
  2. 新 App 优先复用已知模式,不要重新发明轮子
  3. 每次编译错误都是一个学习机会——记录它、理解它、避免它

11.5 四篇博客的总结

本系列的四篇技术博客,从不同角度深入剖析了 ArkUI 开发的方方面面:

  1. 白噪音博客:动画系统 + 多媒体 API + 性能优化
  2. 时间胶囊博客:数据持久化 + 声明式 Builder 语法 + 编译错误修复
  3. 冰箱剩菜博客:Tab 架构 + 游戏化设计 + Grid 弹窗 + 颜色编码
  4. 尴尬粉碎机博客:静态数据管理 + 仪式感交互 + 四款 App 的架构演进

希望这个系列能为 HarmonyOS 应用开发者提供有价值的技术参考。


附录 A:完整文件结构

entry/src/main/ets/pages/Index.ets (953 行)
├── 导入区
├── 类型定义
│   ├── EmbarrassmentEntry(日记条目)
│   ├── TopicCard(话题卡片)
│   ├── ComebackItem(金句条目)
│   └── ColorScheme(颜色主题)
├── 常量定义
│   ├── STORAGE_KEY
│   ├── TOPICS[20](预设话题)
│   ├── COMEBACKS[12](预设金句)
│   └── ALL_COLORS(12 种颜色)
└── @Component struct Index
    ├── @State 变量(10 个)
    ├── 生命周期(aboutToAppear/Disappear)
    ├── build() 方法
    ├── @Builder 方法(15 个)
    │   ├── 通用:buildHeader / buildTabBar / buildTabItem
    │   ├── Tab 0:buildTodayPage / buildTipItem
    │   ├── Tab 1:buildComebackPage
    │   ├── Tab 2:buildDiaryPage / buildDiaryEmpty / buildEntryCard / buildEntrySwipe
    │   └── 弹窗:buildShredAnimation / buildAddEntryDialog / buildDetailDialog
    └── 业务方法(9 个)
        ├── nextTopic / nextComeback / copyComeback
        ├── openAddEntry / addEntry
        ├── startShred / deleteEntry
        ├── loadData / saveData / formatDate

附录 B:颜色主题速查表

名称 色值 用途
primary #FF6B6B 主色、Tab 选中、粉碎按钮
primaryDark #EE5A5A 标题
accent #FFD93D 强调色
accent2 #6BCB77 收藏按钮
accent3 #4D96FF 话题标签、切换按钮
text #2D3436 正文
textLight #B2BEC3 辅助文字、删除按钮
cardBg #FFFFFF 卡片背景
danger #FF6B6B 危险操作
border #F0E6E6 边框

附录 C:四款 App 核心数据对比

指标 白噪音 时间胶囊 冰箱剩菜 尴尬粉碎机
预设数据量 6 种声音 6 个图标 8 分类 + 13 单位 20 话题 + 12 金句
@State 变量 8 8 14 10
@Builder 方法 8 12 17 15
业务方法 8 11 19 9
代码行数 ~767 ~955 ~1320 ~953
首次编译错误 16 17 22 1
数据持久化
Tab 架构

本文是"鸿蒙 Next 应用开发实战"系列的第四篇。由 AtomCode 基于 HarmonyOS Next API 24 编写,记录了尴尬粉碎机 App 的完整开发过程,以及四款 App 的架构演进与模式总结。

(全文完,约 12000 字)

Logo

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

更多推荐