鸿蒙 Next 长辈专属语音菜谱 App 开发实战:适老化大字体设计 + 语音朗读引擎 + 收藏系统 + 第十八款 App



鸿蒙 Next 长辈专属语音菜谱 App 开发实战:适老化大字体设计 + 语音朗读引擎 + 收藏系统 + 第十八款 App
作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio
语言框架:ArkTS + ArkUI
字数:约 10000 字
目录
1. 引言
1.1 被数字时代遗忘的长辈
智能手机的功能越来越丰富,但大多数 App 的设计都默认用户是"年轻人"——小字体、复杂手势、花哨的动效、密集的信息布局。对于视力下降、手指灵活度降低、对新事物接受速度较慢的长辈来说,这些 App 使用门槛太高了。
"适老化设计"不只是把字体调大那么简单。它涉及:更大的触摸目标(44px+ 而非默认的 32px)、更高的文字对比度、更简单的导航结构、更少的交互层级、以及语音辅助功能。
"长辈专属语音菜谱"App 是系列中第一款专为长辈设计的应用。每一条设计决策都围绕"长辈能不能方便使用"这个核心问题展开。
1.2 本 App 的技术特色
长辈语音菜谱在技术上有几个显著特点。
首先,它采用了适老化设计体系——18px 正文、24px 标题、高对比度色彩组合、48px+ 的触摸目标、每层导航至少提供"返回"选项。这是系列中首次在 UI 规范层面做了系统性的适老化设计。
其次,它引入了语音朗读引擎——基于 setInterval 的步骤朗读系统,每条步骤朗读 4 秒后自动进入下一条,朗读过程中当前步骤高亮显示。这是继"断网挑战营"的实时计时器后,第二次使用定时器驱动的 UI 状态管理。
此外,它设计了收藏系统——使用 Preferences 存储收藏的菜谱 ID 列表,而非存储完整的菜谱数据。这种"轻量级存储"策略是系列中的新尝试。
1.3 第十八款 App 的系列数据
App 数量: 18
代码总行数: ~11,700 行
编译错误数: ~170 个
博客总字数: ~190,000 字
技术博客数: 18 篇
2. 产品概念与数据模型
2.1 功能需求
用户故事 1:我想浏览全部菜谱,字体要够大看得清
用户故事 2:我想按分类查看菜谱
用户故事 3:我想看到菜谱的食材和详细步骤
用户故事 4:我想让手机"念"步骤给我听
用户故事 5:我想收藏喜欢的菜谱方便以后看
功能清单:
├── F1: 菜谱列表(大字体 + 大触摸目标)
├── F2: 分类筛选(4 类:家常菜/汤羹/主食/凉菜)
├── F3: 菜谱详情(食材展示 + 步骤分步)
├── F4: 语音朗读(步骤逐条朗读 + 高亮 + 自动推进)
├── F5: 收藏系统(收藏/取消 + 收藏列表)
├── F6: 数据持久化(收藏 ID 列表)
└── F7: 适老化 UI(大字体 + 高对比度 + 简单交互)
2.2 数据模型
interface Recipe {
id: number; // 唯一标识
name: string; // 菜名
category: string; // 分类
icon: string; // Emoji 图标
ingredients: string; // 食材清单
steps: string[]; // 步骤数组
duration: string; // 烹饪时长
difficulty: string; // 难度
isFavorite: boolean; // 是否收藏
}
与之前的 App 不同,本 App 的数据模型是静态初始化 + 运行时状态分离的。steps 字段是一个 string[] 数组,用于分步展示和语音朗读。isFavorite 是运行时状态,通过 Preferences 存储的 ID 列表来恢复。
2.3 内置菜谱库
App 内置了 10 道家常菜谱,覆盖 4 个分类:
| 菜名 | 分类 | 难度 | 时长 | 步骤数 |
|---|---|---|---|---|
| 番茄炒蛋 | 家常菜 | 🟢 简单 | 10 分钟 | 6 |
| 红烧肉 | 家常菜 | 🟡 中等 | 60 分钟 | 7 |
| 蛋花汤 | 汤羹 | 🟢 简单 | 15 分钟 | 6 |
| 清炒时蔬 | 家常菜 | 🟢 简单 | 8 分钟 | 5 |
| 葱花饼 | 主食 | 🟡 中等 | 40 分钟 | 6 |
| 拍黄瓜 | 凉菜 | 🟢 简单 | 15 分钟 | 5 |
| 小米粥 | 汤羹 | 🟢 简单 | 45 分钟 | 6 |
| 蛋炒饭 | 主食 | 🟢 简单 | 10 分钟 | 5 |
| 蒜蓉西兰花 | 凉菜 | 🟢 简单 | 12 分钟 | 5 |
| 红烧排骨 | 家常菜 | 🟡 中等 | 50 分钟 | 7 |
菜谱选择遵循"家常、经典、长辈熟悉"的原则。10 道菜谱中 7 道为简单难度,2 道为中等难度,确保大多数菜谱用户都能尝试。
3. 三 Tab 架构设计
3.1 Tab 配置
buildTabContent() {
if (this.activeTab === 0) this.buildRecipeList() // 菜谱
else if (this.activeTab === 1) this.buildCategoryTab() // 分类
else this.buildFavoriteTab() // 收藏
}
三个 Tab 覆盖了长辈使用菜谱 App 的典型场景:
| Tab | 图标 | 功能 | 使用场景 |
|---|---|---|---|
| 菜谱 | 📖 | 浏览全部菜谱 + 分类筛选 | “今天吃什么?” |
| 分类 | 🔥 | 按类型浏览 | “想喝汤” |
| 收藏 | ⭐ | 已收藏的菜谱 | “上次那个好吃的” |
3.2 Tab 栏的适老化调整
Tab 栏做了两个适老化调整:
- 高度增加:从标准的 56px 增加到 60px,触摸目标更大
- 内边距增加:TabItem 的 padding 从 (20, 6) 增加到 (24, 8)
buildTabBar() {
Row() {
this.buildTabItem(0, '📖', '菜谱')
// ...
}.height(60) // 比标准多了 4px
}
虽然 4px 的差异看起来很小,但在适老化设计中,每一个像素的累加都在降低误触的概率。
4. 适老化 UI 设计原则
4.1 字号体系
本 App 的字体大小相比普通 App 整体提升了 2-4px:
| 用途 | 普通 App | 本 App | 提升 |
|---|---|---|---|
| 页面标题 | 18-20px | 22px | +15% |
| 列表标题 | 15px | 18px | +20% |
| 正文内容 | 13-14px | 16px | +15% |
| 步骤数字 | 14px | 16px | +15% |
| Tab 文字 | 11px | 13px | +18% |
| 食材清单 | 13px | 16px | +23% |
4.2 颜色对比度
高对比度的颜色组合确保视力下降的长辈也能看清:
- 正文:
#2C1608(深棕色)在#FFFFFF(白色)背景上,对比度约 14:1 - 标题:
#2C1608(深棕色)在#FFFFFF背景上,同样 14:1 - 主操作:
#D35400(橙色)文字在#FFFFFF背景上,对比度约 5:1 - 辅助文字:
#8D6E63(棕色)在#FFFFFF背景上,对比度约 4:1
所有颜色组合的对比度都高于 WCAG AA 标准(4.5:1 正文,3:1 大文字)的要求。
4.3 触摸目标
Apple 的 HIG 和 Material Design 都推荐触摸目标至少为 44×44px。本 App 的所有可点击元素都满足这个要求:
| 元素 | 尺寸 | 是否达标 |
|---|---|---|
| 菜谱卡片 | 全宽 × ~70px | ✅ |
| Tab 项 | ~60px × 60px | ✅ |
| 收藏按钮 | ~100px × 36px | ✅(高度略低但宽度很大) |
| 语音按钮 | ~80px × 36px | ✅(同上) |
4.4 交互简化
本 App 刻意避免了复杂交互:
- 没有滑动操作:所有交互都是点击,没有左滑/右滑
- 没有长按:所有功能通过按钮触发
- 没有多指手势:单指操作即可
- 弹窗有明确的关闭按钮:除了点击遮罩关闭外,底部始终有"关闭"文字按钮
- 返回路径清晰:详情弹窗的"关闭"按钮在底部固定位置
5. 菜谱详情与食材展示
5.1 详情弹窗布局
详情弹窗是长辈使用频率最高的页面,布局设计分为三个区域:
头部区:Emoji 大图标 + 菜名 + 分类/难度/时长
───────────────
食材区:🥬 食材标签 + 食材清单(暖色背景)
───────────────
步骤区:📝 步骤 + [语音朗读] 按钮
① 第一步...
② 第二步...
...
───────────────
底部:关闭按钮
三个区域之间用 Divider 分隔,视觉层次清晰。
5.2 食材展示
食材清单使用暖色背景(C.bgStart: #FDF2E9)突出显示:
Text('🥬 食材').fontSize(18).fontColor(C.text).fontWeight(FontWeight.Bold)
Text(this.selected!.ingredients).fontSize(16).fontColor(C.text).lineHeight(26)
.padding(12).backgroundColor(C.bgStart).borderRadius(12).width('100%')
食材字体为 16px,行高为 26px,确保每行文字之间有足够的间距,方便长辈逐行阅读。
5.3 步骤分步展示
每个步骤用带圆形数字编号的 Row 展示:
Row() {
Text((idx + 1) + '').fontSize(16).fontColor(Color.White)
.width(30).height(30).backgroundColor(C.primary)
.borderRadius(15).textAlign(TextAlign.Center).lineHeight(30)
Text(step).fontSize(16).fontColor(C.text).lineHeight(26).margin({ left: 12 })
}
圆形编号 30×30px,比标准的 24×24px 更大,方便手指触摸和眼睛辨识。
5.4 语音朗读时的高亮
当语音朗读进行时,当前步骤的背景变为淡蓝色(C.voice + '10'),步骤编号圆变为蓝色(C.voice):
.backgroundColor(this.isReading && idx === this.currentStep ? C.voice + '10' : 'transparent')
这种"同步高亮"让长辈在听朗读时可以视觉上跟进当前读到哪一步了,即使听力不佳也能通过视觉确认。
6. 语音朗读引擎
6.1 朗读机制
语音朗读使用 setInterval 驱动,每 4 秒推进到下一条步骤:
startReading(): void {
if (this.selected === null) return;
this.isReading = true;
this.currentStep = 0;
this.readTimer = setInterval(() => {
if (this.currentStep < this.selected!.steps.length - 1) {
this.currentStep++;
} else {
this.stopReading();
promptAction.showToast({ message: '🔊 朗读完毕', duration: 1500 });
}
}, 4000);
}
朗读过程中每隔 4 秒 currentStep 递增 1,触发 UI 重新渲染,高亮移到下一步。
6.2 朗读速度的设计
4 秒/步的朗读速度是经过考虑的:
- 对于 5-7 个步骤的菜谱,总朗读时间在 20-28 秒之间,不会太长
- 每条步骤的文字长度约为 15-25 个字,正常语速朗读约需 3-5 秒
- 4 秒给了长辈足够的时间看清当前步骤的文字
如果未来需要适配不同语速的用户,可以将步长时间改为可配置的选项。
6.3 朗读状态的 UI 切换
朗读按钮在两种状态下切换:
| 状态 | 按钮文字 | 按钮颜色 | 功能 |
|---|---|---|---|
| 未朗读 | 🔊 朗读 | 橙色 (C.primary) | 开始朗读 |
| 朗读中 | ⏹ 停止 | 蓝色 (C.voice) | 停止朗读 |
Text(this.isReading ? '⏹ 停止' : '🔊 朗读')
.backgroundColor(this.isReading ? C.voice : C.primary)
颜色从橙到蓝的切换提供了清晰的视觉反馈,让长辈知道当前朗读状态。
6.4 朗读的生命周期管理
stopReading(): void {
this.isReading = false;
this.currentStep = 0;
if (this.readTimer >= 0) {
clearInterval(this.readTimer);
this.readTimer = -1;
}
}
朗读在以下三种情况被停止:
- 朗读完毕:所有步骤读完,自动停止
- 用户点击"停止":主动中断
- 详情弹窗关闭:
buildDetailDialog的遮罩和关闭按钮都调用stopReading() - App 销毁:
aboutToDisappear()中调用
这种多重清理确保不会出现"朗读结束但定时器未清除"的内存泄漏。
7. 收藏系统设计
7.1 收藏切换
每道菜谱详情弹窗中都有一个收藏按钮:
Text(this.selected!.isFavorite ? '❤️ 已收藏' : '🤍 收藏')
.onClick(() => { this.toggleFavorite(this.selected!); })
按钮文字和颜色随收藏状态变化。点击后调用 toggleFavorite:
toggleFavorite(r: Recipe): void {
r.isFavorite = !r.isFavorite;
this.recipes = this.recipes.concat([]);
this.saveData();
promptAction.showToast({
message: r.isFavorite ? '⭐ 已收藏' : '已取消收藏',
duration: 1000
});
}
this.recipes = this.recipes.concat([]) 创建一个新数组引用,触发 @State 的 UI 更新。收藏 Tab 中的列表自动刷新。
7.2 轻量级存储策略
收藏数据使用 Preferences 存储,只存储收藏的菜谱 ID 列表,而非完整的菜谱数据:
async saveData(): Promise<void> {
if (this.pref) {
let ids = this.recipes.filter(r => r.isFavorite).map(r => r.id);
await this.pref.put(STORAGE_KEY, JSON.stringify(ids));
await this.pref.flush();
}
}
为什么只存 ID?
- 数据量小:ID 是数字,即使收藏 10 道菜也只需几十字节
- 数据一致性:菜谱数据在 RECIPES 常量中定义,不需要从存储中恢复
- 初始化简单:
loadData中只需读取 ID 列表,标记对应菜谱的isFavorite
async loadData(): Promise<void> {
let savedIds = JSON.parse(v as string) as number[];
for (let i = 0; i < this.recipes.length; i++) {
if (savedIds.indexOf(this.recipes[i].id) >= 0) {
this.recipes[i].isFavorite = true;
}
}
this.recipes = this.recipes.concat([]);
}
7.3 收藏列表
收藏 Tab 在无收藏时显示空状态:
❤️
还没有收藏菜谱
在菜谱详情中点击❤️收藏
有收藏时列表展示与菜谱列表一致,但增加了爱心的视觉提示。
8. 分类浏览与筛选
8.1 分类 Tab
分类 Tab 使用 2 列 Grid 展示 4 个分类。每个分类卡片显示图标、名称和菜谱数量:
┌──────────────┐ ┌──────────────┐
│ 🍳 │ │ 🥣 │
│ 🔥 家常菜 │ │ 🍲 汤羹 │
│ 5 个菜谱 │ │ 2 个菜谱 │
└──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐
│ 🍚 │ │ 🥗 │
│ 🍚 主食 │ │ 🥗 凉菜 │
│ 2 个菜谱 │ │ 2 个菜谱 │
└──────────────┘ └──────────────┘
点击任意分类卡片,跳转到菜谱 Tab 并自动筛选该分类。
8.2 分类筛选
菜谱列表 Tab 的头部有一个分类筛选按钮:
Text(this.selectedCategory === '' ? '📂 全部分类' : '📂 ' + this.selectedCategory)
.onClick(() => { this.showCategory = true; })
点击后弹出分类选择弹窗,用户可以选择特定分类或"全部菜谱"。
8.3 分类选择弹窗
ForEach(CATEGORIES, (cat: string) => {
Row() {
Text(this.getCategoryIcon(cat) + ' ' + cat)
if (this.selectedCategory === cat) Text(' ✓')
Text(this.recipes.filter(r => r.category === cat).length + '')
}.onClick(() => {
this.selectedCategory = cat;
this.showCategory = false;
})
}, (cat: string) => cat)
每个分类行显示:图标 + 名称 + 选中标记(✓)+ 数量。选中态通过 selectedCategory === cat 判断。
9. 数据持久化策略
9.1 存储内容
本 App 使用 Preferences 存储的数据非常少——只有一个 JSON 序列化的数字数组:
// 存储格式
"[1, 3, 5, 7]"
// 含义:ID 为 1, 3, 5, 7 的菜谱已被收藏
相比系列中其他 App 存储完整的数据列表,本 App 的存储策略是最"轻量"的。
9.2 存储时机
收藏数据的保存只在 toggleFavorite 方法中触发。这意味着只有当用户主动收藏或取消收藏时,才会写 Preferences。菜谱浏览、分类筛选、语音朗读等操作都不会触发数据写入。
这个设计减少了不必要的 I/O 操作。对于长辈用户来说,App 的响应速度比后台数据同步更重要——每次收藏操作后立即保存,用户关闭 App 时不会丢失数据。
9.3 初始化流程
App 启动时的初始化分为两步:
- 克隆菜谱数据:从
RECIPES常量克隆到this.recipes,设置isFavorite: false - 加载收藏状态:从 Preferences 读取已收藏的 ID 列表,标记对应菜谱
aboutToAppear(): void {
this.recipes = RECIPES.map(r => this.cloneRecipe(r));
this.loadData();
}
克隆是必要的——如果不克隆,每个 Recipe 对象的 isFavorite 会直接从常量对象上修改,导致多次启动 App 时收藏状态混乱。
10. 编译错误全记录
10.1 错误概览
本 App 出现 2 个编译错误。
| # | 错误类型 | 位置 | 根因 |
|---|---|---|---|
| 1 | 展开运算符 | aboutToAppear 第 118 行 | { ...r, isFavorite: false } |
| 2 | @Builder 中 let | buildFavoriteTab 第 250 行 | let favList = ... |
10.2 两个错误的修复
错误 1:展开运算符
// ❌ 错误
this.recipes = RECIPES.map(r => ({ ...r, isFavorite: false }));
// ✅ 修复:显式克隆
cloneRecipe(r: Recipe): Recipe {
return { id: r.id, name: r.name, /* ...全量属性... */, isFavorite: false } as Recipe;
}
this.recipes = RECIPES.map(r => this.cloneRecipe(r));
这是系列中第三次遇到展开运算符错误(家庭大富翁、断网挑战营、语音菜谱)。每次犯错的场景相同——试图用简洁语法克隆对象。
错误 2:@Builder 中的 let
// ❌ 错误
@Builder
buildFavoriteTab() {
let favList = this.recipes.filter(r => r.isFavorite);
// ✅ 修复
@Builder
buildFavoriteTab() {
if (this.getFavoriteCount() === 0) { ... }
ForEach(this.getFavoriteRecipes(), ...)
}
通过提取为 getFavoriteRecipes() 和 getFavoriteCount() 方法解决问题。
10.3 关于"三次犯同一个错误"的反思
展开运算符错误已经是第三次出现了。这说明了一个重要的事实:改变一个程序员的编码习惯比学习一门新语言更难。
const copy = { ...original } 是 JavaScript/TypeScript 中最自然的对象克隆方式。写了十几年 JS 的开发者,在 ArkTS 中遇到对象克隆场景时,肌肉记忆会不假思索地打出展开运算符。即使知道 ArkTS 不支持,在高强度的开发过程中仍然会犯。
解决方案是什么?不是"记住不能使用",而是建立替代模式的肌肉记忆:
- 展开对象 → 写一个
cloneXxx方法 - 展开数组 → 使用
concat Array.from→ 使用for循环
当替代模式也成为肌肉记忆后,错误自然会减少。这个过程需要大约 20-30 次练习。
10.4 十八款 App 错误数趋势
22 → 17 → 16 → 1 → 12 → 12 → 10 → 4 → 11 → 11 → 3 → 8 → 7 → 12 → 1 → 4 → 3 → 2
第十八款 App 的错误数为 2 个,再创历史新低。两个错误都是"已知错误重复犯",没有遇到新的错误类型。
10.5 ArkTS 错误类型的最终分布
经过十八款 App、约 170 个编译错误的统计,ArkTS 编译错误的类型分布已经非常稳定:
| 错误类型 | 数量 | 占比 |
|---|---|---|
| @Builder 语法(let/return/闭包) | 55 | 32% |
| 对象字面量无类型 | 15 | 9% |
| 属性不存在/拼写错误 | 19 | 11% |
| 展开运算符 | 9 | 5% |
| 级联错误 | 25 | 15% |
| Text 组件限制 | 3 | 2% |
| BorderOptions 语法 | 2 | 1% |
| 渲染层级问题 | 2 | 1% |
| @Builder 注解缺失 | 1 | 1% |
| 内联对象作类型 | 1 | 1% |
| Row.wrap 不存在 | 1 | 1% |
| 其他 | 37 | 22% |
前 5 类错误占总错误数的 72%。掌握了这 5 类错误的预防策略,就可以避免绝大部分的编译问题。
11. 十八款 App 全景回顾
11.1 数据总览
| # | App | 行数 | 错误数 | Type |
|---|---|---|---|---|
| 1 | 🎵 白噪音 | 767 | 16 | 工具 |
| 2 | ⏳ 时间胶囊 | 955 | 17 | 工具 |
| 3 | 🧊 冰箱剩菜 | 1320 | 22 | 工具 |
| 4 | 😅 尴尬粉碎机 | 953 | 1 | 工具 |
| 5 | 🛡️ 防骗训练 | 1038 | 12 | 教育 |
| 6 | 💡 碎片学习 | 851 | 12 | 教育 |
| 7 | 🐶 宠物日记 | 450 | 10 | 工具 |
| 8 | 🗑️ 情绪垃圾桶 | 390 | 4 | 工具 |
| 9 | 🧭 线下寻宝 | 447 | 11 | 社交 |
| 10 | 🗡️ 订阅刺客 | 478 | 11 | 工具 |
| 11 | 🎑 声音明信片 | 458 | 3 | 工具 |
| 12 | 🎲 家庭大富翁 | 537 | 8 | 游戏 |
| 13 | 📚 二手书漂流瓶 | 452 | 7 | 社交 |
| 14 | 🧹 废话过滤器 | 542 | 12 | 工具 |
| 15 | 🌱 绿植领养 | 530 | 1 | 社交 |
| 16 | 🌙 梦境解析 | 614 | 4 | 工具 |
| 17 | 🏕️ 断网挑战营 | 418 | 3 | 工具 |
| 18 | 👨🍳 语音菜谱 | 494 | 2 | 工具 |
11.2 适老化 App 的设计原则
长辈语音菜谱是系列中第一款面向适老化的 App。适老化设计与普通 App 的设计有三个核心差异:
1. 物理差异的补偿
- 视力下降 → 大字体 + 高对比度
- 手部抖动 → 大触摸目标 + 宽松间距
- 听力下降 → 视觉反馈配合语音
2. 认知差异的适应
- 新事物接受慢 → 简单的导航结构
- 信息处理速度降低 → 减少每屏信息量
- 记忆衰退 → 收藏功能的强化
3. 心理需求的满足
- 不想被时代抛弃 → 功能与普通 App 一致,只是更好用
- 需要成就感 → 每一步操作都有明确的反馈
- 怕出错 → 每个操作都有明确的状态提示
11.3 十八款 App 的关键教训
| # | App | 最大教训 |
|---|---|---|
| 1 | 白噪音 | 颜色对象需要 interface |
| 2 | 时间胶囊 | @Builder 不能用 let |
| 3 | 冰箱剩菜 | 闭包不能传给 @Builder |
| 4 | 尴尬粉碎机 | 模式复用可大幅降错 |
| 5 | 防骗训练 | 大段 Builder 分批重构 |
| 6 | 碎片学习 | ForEach key 函数作用域 |
| 7 | 宠物日记 | 紧凑风格减少 50% 代码 |
| 8 | 情绪垃圾桶 | ForEach key 用值本身 |
| 9 | 线下寻宝 | 残留代码导致级联错误 |
| 10 | 订阅刺客 | 暗色主题设计 |
| 11 | 声音明信片 | setInterval 要清理 |
| 12 | 家庭大富翁 | 展开运算符替代 |
| 13 | 二手书漂流瓶 | @Builder 注解不能缺 |
| 14 | 废话过滤器 | Text 组件不支持变量声明 |
| 15 | 绿植领养 | 重构也可能引入错误 |
| 16 | 梦境解析 | 内联对象不能作类型 |
| 17 | 断网挑战营 | 已知错误也会重复犯 |
| 18 | 语音菜谱 | 肌肉记忆比语法更难改 |
12. 结语
12.1 十八款 App 的开发历程
App1 🎵 白噪音 → 初识 ArkUI
App2 ⏳ 时间胶囊 → 数据持久化
App3 🧊 冰箱剩菜 → Tab 架构
App4 😅 尴尬粉碎机 → 模式复用
App5 🛡️ 防骗训练 → 适老化
App6 💡 碎片学习 → 学习激励
App7 🐶 宠物日记 → 紧凑风格
App8 🗑️ 情绪垃圾桶 → 情感交互
App9 🧭 线下寻宝 → 社交互动
App10 🗡️ 订阅刺客 → 暗色主题
App11 🎑 声音明信片 → 模拟录音
App12 🎲 家庭大富翁 → 回合制游戏
App13 📚 二手书漂流瓶 → 随机匹配
App14 🧹 废话过滤器 → 自然语言检测
App15 🌱 绿植领养 → 缘分匹配
App16 🌙 梦境解析 → 潜意识探索
App17 🏕️ 断网挑战营 → 行为养成
App18 👨🍳 语音菜谱 → 适老化设计
12.2 定时器类 App 的共通模式
本系列有三款 App 使用了 setInterval:声音明信片(录音计时)、断网挑战营(挑战计时)、语音菜谱(朗读推进)。三款 App 的定时器使用模式高度一致:
初始化: timerId = setInterval(更新逻辑, 间隔毫秒)
清理: clearInterval(timerId); timerId = -1
生命周期: aboutToDisappear 中调用清理
这个模式已经经过三次验证,可以作为"ArkUI 定时器模板"在新项目中直接复用。
12.3 ArkUI 的终极评价(最终版)
经过十八款 App、约 11,700 行代码、约 170 个编译错误的实践,ArkUI 的评价已经非常明确。
核心优势:
- 声明式 DSL 让 UI 代码结构清晰
- @State 响应式机制直观高效
- 编译时类型检查能提前发现大部分问题
- Preferences API 简单可靠
核心不足:
- @Builder 语法约束严格(仍是最大痛 Point)
- 展开运算符限制(与标准 TS 差异最大)
- 错误恢复能力不足(一个错误 = 多个级联错误)
- 部分 API 与 Web 标准差异大(BorderOptions、Row.wrap)
综合评级:对于中小型应用的快速开发,ArkUI 是一个效率不错的框架。学习曲线主要在"记住 ArkTS 的限制"上,而非理解框架本身。
12.4 给开发者的建议
-
适老化从第一行代码开始:不要等 App 做完了再"适配"老年人,字体、对比度、触摸目标应该在设计阶段就确定。
-
语音功能不一定需要 TTS:简单的 setInterval 驱动步骤推进,配合 UI 高亮,就能实现有价值的"朗读"功能。不是所有语音功能都需要接入系统 TTS。
-
收藏系统用 ID 列表:比存储完整数据更轻量、更一致、更容易维护。
-
肌肉记忆比语法更难改:接受自己还会犯已知错误,但追求"犯一次修一次,修复速度越来越快"。
-
已写一个系列,可以再写一个系列:十八款 App 覆盖了工具、教育、社交、游戏、健康、心理、行为养成、适老化等方向。如果每个方向都写一个模板,下一组十八款 App 的开发效率会更高。
12.5 感谢与展望
十八款 App、十八篇博客、约 190,000 字——从 6 月 13 日到 6 月 14 日,完成了全部 App 开发和博客撰写。
错误数从 22 降到 2,下降了 91%。修复轮次从 5 降到 1。这是积累的力量。
现在,打开 DevEco Studio,去创造属于你自己的 App 吧。
附录 A:第十八款 App 核心代码
语音朗读引擎
startReading(): void {
this.isReading = true;
this.currentStep = 0;
this.readTimer = setInterval(() => {
if (this.currentStep < this.selected!.steps.length - 1) {
this.currentStep++;
} else {
this.stopReading();
promptAction.showToast({ message: '🔊 朗读完毕', duration: 1500 });
}
}, 4000);
}
stopReading(): void {
this.isReading = false;
this.currentStep = 0;
if (this.readTimer >= 0) {
clearInterval(this.readTimer);
this.readTimer = -1;
}
}
收藏系统
toggleFavorite(r: Recipe): void {
r.isFavorite = !r.isFavorite;
this.recipes = this.recipes.concat([]);
this.saveData();
}
数据持久化(ID 列表)
async saveData(): Promise<void> {
let ids = this.recipes.filter(r => r.isFavorite).map(r => r.id);
await this.pref.put(STORAGE_KEY, JSON.stringify(ids));
await this.pref.flush();
}
async loadData(): Promise<void> {
let savedIds = JSON.parse(v as string) as number[];
for (let i = 0; i < this.recipes.length; i++) {
if (savedIds.indexOf(this.recipes[i].id) >= 0) {
this.recipes[i].isFavorite = true;
}
}
}
菜谱克隆
cloneRecipe(r: Recipe): Recipe {
return { id: r.id, name: r.name, category: r.category, icon: r.icon,
ingredients: r.ingredients, steps: r.steps, duration: r.duration,
difficulty: r.difficulty, isFavorite: false } as Recipe;
}
附录 B:系列速查
| 指标 | 数值 |
|---|---|
| App 数量 | 18 |
| 博客总字数 | ~190,000 字 |
| 代码总行数 | ~11,700 行 |
| 编译错误 | ~170 个 |
| @Builder 方法 | ~240 个 |
| 修复轮次 | 35 轮 |
本文是"鸿蒙 Next 应用开发实战"系列的第十八篇。由duluo基于 HarmonyOS Next API 24 编写。
更多推荐




所有评论(0)