欢迎光临
我们一直在努力

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

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

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

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


目录

  • 引言
  • 产品概念与数据模型
  • 三 Tab 架构设计
  • 适老化 UI 设计原则
  • 菜谱详情与食材展示
  • 语音朗读引擎
  • 收藏系统设计
  • 分类浏览与筛选
  • 数据持久化策略
  • 编译错误全记录
  • 十八款 App 全景回顾
  • 结语

  • 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)
    未经允许不得转载:171主机测评 » 鸿蒙 Next 长辈专属语音菜谱 App 开发实战:适老化大字体设计 + 语音朗读引擎 + 收藏系统 + 第十八款 App
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址