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

鸿蒙 Next 家庭记忆时光机 App 开发实战:记忆时间线 + 情感化设计 + 预览器兼容性

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


目录

  1. 引言
  2. 产品概念与记忆模型
  3. 三 Tab 架构设计
  4. 时间线实现
  5. 添加记忆与表单设计
  6. 记忆详情弹窗
  7. 统计概览
  8. 预览器兼容性修复
  9. 编译错误全记录
  10. 从复杂到极简的演进
  11. 第二十八款 App 全景回顾
  12. 结语

1. 引言

1.1 为什么需要"记忆时光机"

你有没有这样的时刻——

翻到一张老照片,看到照片里的场景,但想不起来那是哪一年、在哪里、当时发生了什么。

手机里有几千张照片,但真正被翻看的,可能不到 1%。

"家庭记忆时光机"要解决的问题很简单:用文字留住记忆。不是替代照片,而是在照片之外,用几段话记录下那些"当时觉得普通、回头看却很珍贵"的瞬间。

1.2 与同类 App 的差异

市面上已经有大量的日记 App、笔记 App、相册 App。但"家庭记忆时光机"的定位不同:

日记 App 相册 App 本 App
核心内容 每日心情 照片 精选记忆
记录频率 每天 随时 值得记的才记
呈现方式 按日期列表 按时间网格 卡片时间线
情感设计 中性 中性 温暖复古
目标用户 个人 个人 家庭

本 App 的核心理念是:少即是多。不要求每天记录,只记录那些"值得记住"的瞬间。10 条精心挑选的回忆,比 1000 张无人问津的照片更有温度。

1.3 家庭记忆的特殊性

家庭记忆和个人记忆有一个本质区别:所有权

个人日记是"我的记忆"——写给自己看的,不需要考虑读者的感受。但家庭记忆是"我们的记忆"——写的时候会想着"以后孩子看到这条会怎么想"、“老伴看到这条会不会笑”。

这种"为他人记录"的心态影响了 App 的设计:

个人日记 → 随性、情绪化、可能负能量
家庭记忆 → 精选、温暖、正能量的片段

所以本 App 在设计时有意去掉了"今日心情"、"情绪标签"等功能——不是它们不好,而是它们更适合个人日记场景。家庭记忆场景需要的是:干净的记录、温暖的回看、简单的分享。

1.4 视觉风格的怀旧感

本 App 的视觉设计刻意营造了一种"旧时光"的氛围:

  • 主色 #B8860B(复古金):这是古代书画中常用的金色调,带有时间的厚重感
  • 背景 #F5EDE0(米黄):模仿老照片泛黄的色调
  • 深棕文字 #3D2B1F:像旧书页上的油墨

颜色本身就有情感属性。冷色调(蓝、灰)传达"高效、专业",暖色调(米黄、金棕)传达"温暖、怀旧"。家庭记忆 App 选择了暖色调——每次打开都像翻开一本旧相册。

1.5 本 App 的技术特色

  1. 卡片时间线:按时间倒序排列的卡片列表,每条记忆以 emoji + 标题 + 摘要展示
  2. 极简表单:标题输入 + 年份选择,快速记录
  3. 详情弹窗:点击卡片查看完整内容
  4. 预览器兼容:从设计之初就排除了预览器不支持的 API
  5. 渐进式复杂度:从 479 行精简到 188 行,移除所有不必要的抽象

1.6 二十八款 App 全景

App 数量:    28
代码总行数:  ~16,600 行
编译错误数:  ~266 个
博客总字数:  ~282,000 字
技术博客数:  28 篇

2. 产品概念与记忆模型

2.1 功能需求

用户故事 1:我想把一家人的快乐瞬间记下来
用户故事 2:我想按时间顺序翻看以前的记忆
用户故事 3:我想给每段记忆配一个表情符号
用户故事 4:我想知道一共记录了多少段记忆

功能清单:
├── F1: 记忆时间线(按时间倒序)
├── F2: 添加记忆(标题 + 年份)
├── F3: 记忆详情弹窗
├── F4: 统计概览(总条数)
├── F5: 预置示例记忆
└── F6: 三 Tab 切换

2.2 数据模型

interface Memory {
  id: number;         // 唯一标识
  date: number;       // 时间戳
  year: number;       // 年份
  emoji: string;      // 情绪/主题图标
  title: string;      // 记忆标题
  content: string;    // 记忆正文
}

这是本系列中最简洁的数据模型之一。6 个字段,涵盖了记忆的核心要素:时间(date + year)、情感(emoji)、标题、内容。

为什么不用照片:照片存储需要文件 I/O 权限,在预览器中不可用。文字版记忆可以在预览器中完整运行。

2.3 预置示例

const DEMO: Memory[] = [
  { id: 1, date: 1703462400000, year: 2024, emoji: '🎄', title: '圣诞节', content: '一起装饰了圣诞树,吃了火锅。' },
  { id: 2, date: 1727856000000, year: 2024, emoji: '🏖️', title: '海边旅行', content: '第一次看到大海,很开心。' },
  { id: 3, date: 1696032000000, year: 2023, emoji: '🎂', title: '生日', content: '全家人一起过的生日。' }
];

为什么使用时间戳常量而不是 new Date():在 ArkTS 中,顶层常量的初始化发生在模块加载时。如果使用 new Date(2024, 11, 25),Previewer 需要解析 Date 构造函数,这在某些预览器版本中可能失败。使用毫秒时间戳常量彻底避免了这个问题。


3. 三 Tab 架构设计

3.1 Tab 配置

build() {
  Stack() {
    Column().backgroundColor(C.bg)

    Column() {
      this.buildHeader()
      if (this.activeTab === 0) this.buildTimeline()
      else if (this.activeTab === 1) this.buildAdd()
      else this.buildStats()
      this.buildTabBar()
    }

    if (this.showMemory) this.buildDetail()
    if (this.showAdd) this.buildAddForm()
  }
}
Tab 图标 功能
0 📅 时间线 — 浏览所有记忆
1 ✏️ 添加 — 记录新记忆
2 📊 统计 — 查看总条数

弹窗处理buildDetail()buildAddForm() 使用了最小化的 if 包裹,没有任何额外的生命周期逻辑。弹窗打开和关闭只涉及 @State 变量的设置,不涉及任何异步操作。

3.2 顶部栏

@Builder
buildHeader() {
  Row() {
    Text('🕰️ 记忆时光机').fontSize(20).fontColor(C.text).fontWeight(FontWeight.Bold)
    Blank()
  }.width('100%').padding({ left: 20, right: 20, top: 48, bottom: 8 })
}

本 App 的顶部栏是系列中最简单的——只有一个标题,没有副标题、没有计数、没有操作按钮。所有操作都通过 Tab 和弹窗完成。

3.3 三 Tab 数据流

添加记忆 → memories 数组头部插入 → @State 自动更新
                                           ↓
时间线 Tab ← 读取 memories(按时间倒序展示)
统计 Tab ← 读取 memories.length(总条数)
详情弹窗 ← 通过索引访问 memories[idx]

所有数据流都是单向的——从添加操作到列表更新,再到统计展示。不需要任何中间状态管理。


4. 时间线实现

4.1 卡片列表

@Builder
buildTimeline() {
  Column() {
    if (this.memories.length === 0) {
      Text('暂无记忆').fontSize(16).fontColor(C.textMuted)
    } else {
      Scroll() {
        Column() {
          ForEach(this.memories, (mem: Memory) => {
            Column() {
              Row() {
                Text(mem.emoji).fontSize(28)
                Column() {
                  Text(mem.title).fontSize(16).fontColor(C.text).fontWeight(FontWeight.Bold)
                  Text(mem.year + '年').fontSize(12).fontColor(C.textMuted).margin({ top: 2 })
                  Text(mem.content).fontSize(13).fontColor(C.textLight).maxLines(2)
                }.margin({ left: 10 }).layoutWeight(1)
              }.width('100%').padding(14)
            }.width('100%').backgroundColor(C.bgCard).borderRadius(16).margin({ bottom: 6 })
            .onClick(() => {
              this.selectedMemory = this.memories.indexOf(mem);
              this.showMemory = true;
            })
          }, (mem: Memory) => mem.id.toString())
        }.padding({ left: 16, right: 16 })
      }.layoutWeight(1)
    }
  }.layoutWeight(1)
}

卡片布局:左侧 emoji(28sp)、右侧三行信息(标题 + 年份 + 预览)。maxLines(2) 控制预览行数,超出省略。整个卡片 16px 圆角 + 6px 间距,形成呼吸感。

ForEach 的 key 函数:使用 mem.id.toString() 作为唯一标识。id 是 Date.now() 生成的数字,保证在同一次运行中唯一。

4.2 点击查看详情

.onClick(() => {
  this.selectedMemory = this.memories.indexOf(mem);
  this.showMemory = true;
})

indexOf(mem) 获取该记忆在数组中的索引,然后通过索引去详情弹窗中读取完整内容。这种"引用转索引"的模式避免了在 @Builder 中存储对象引用。


5. 添加记忆与表单设计

5.1 添加入口

@Builder
buildAdd() {
  Column() {
    Blank().layoutWeight(1)
    Text('✏️').fontSize(48)
    Text('记录新记忆').fontSize(18).fontColor(C.primary).fontWeight(FontWeight.Bold).margin({ top: 8 })
    Text('写下来,让温暖不被遗忘').fontSize(14).fontColor(C.textMuted).margin({ top: 4 })
    Blank().layoutWeight(1)
  }.width('100%').alignItems(HorizontalAlign.Center)
  .onClick(() => {
    this.newEmoji = '💖';
    this.newTitle = '';
    this.newContent = '';
    this.newYear = 2025;
    this.showAdd = true;
  })
}

添加 Tab 不像传统表单那样直接展示输入框,而是展示一个"引导页面"——一个大大的 ✏️ 图标,加一句温暖的话,点击后才打开弹窗表单。

这种设计有两个好处:

  1. 降低心理门槛:看到空白的表单会让人犹豫"要写什么",看到引导页只会让人想"点一下看看"
  2. Tab 切换不变:填写表单时如果切换到其他 Tab,弹窗关闭但数据不丢失

5.2 添加弹窗

@Builder
buildAddForm() {
  Column() {
    // 遮罩层
    Column().onClick(() => { this.showAdd = false; })

    // 表单卡片
    Column() {
      Text('记录记忆').fontSize(18).fontColor(C.text)
      TextInput({ placeholder: '标题', text: this.newTitle })
        .fontSize(16).height(44).backgroundColor(C.bg).borderRadius(12)

      Text('保存').fontSize(16).fontColor(Color.White)
        .backgroundColor(C.primary).borderRadius(20)
        .onClick(() => {
          if (this.newTitle.trim().length > 0) {
            const mem: Memory = {
              id: Date.now(), date: Date.now(),
              year: this.newYear, emoji: this.newEmoji,
              title: this.newTitle.trim(), content: this.newContent.trim()
            };
            this.memories = [mem, ...this.memories];
            this.showAdd = false;
          }
        })
    }
  }
}

表单最少字段原则:只有标题是必填项。年份默认当前年,内容可选。最简记录路径:打字 → 点保存 → 完成。整个过程不超过 5 秒。

保存逻辑

  1. 创建 Memory 对象:id = Date.now()(唯一标识)、date = Date.now()(当前时间戳)
  2. 头部插入:[mem, ...this.memories](最新的在最前面)
  3. 关闭弹窗:this.showAdd = false
  4. @State 自动触发时间线 Tab 更新

6. 记忆详情弹窗

6.1 弹窗结构

@Builder
buildDetail() {
  if (this.selectedMemory >= 0 && this.selectedMemory < this.memories.length) {
    Column() {
      // 遮罩层
      Column().backgroundColor('rgba(0,0,0,0.3)')
        .onClick(() => { this.showMemory = false; })

      // 详情卡片
      Column() {
        Text(this.memories[this.selectedMemory].emoji).fontSize(56)
        Text(this.memories[this.selectedMemory].title).fontSize(20)
        Text(this.memories[this.selectedMemory].content).fontSize(15)
        Text('关闭').onClick(() => { this.showMemory = false; })
      }.width('85%').backgroundColor(C.bgCard).borderRadius(24)
    }
  }
}

弹窗的视觉层次从上到下:大 emoji → 标题 → 正文 → 关闭按钮。所有文字通过 this.memories[this.selectedMemory] 从数组索引读取。

为什么不用 const mem = this.memories[this.selectedMemory]:因为 ArkTS 的 @Builder 中不允许 const 声明。所有变量必须通过内联表达式访问。

6.2 安全性检查

if (this.selectedMemory >= 0 && this.selectedMemory < this.memories.length) {

弹窗渲染前检查索引是否越界。这个检查在正常操作中是多余的(selectedMemory 总是通过 indexOf 设置),但防御性编程的习惯可以避免极端情况下的白屏。


7. 统计概览

7.1 统计页

@Builder
buildStats() {
  Column() {
    Blank().layoutWeight(1)
    Text('共 ' + this.memories.length + ' 条记忆').fontSize(20).fontColor(C.text)
    Blank().layoutWeight(1)
  }
}

统计页是系列中最简单的——只有一行文字,显示总记忆条数。没有图表、没有柱状图、没有年度分布。

有意识的设计决策:在 App 的初始版本中,只展示最核心的指标。后续版本可以根据需要添加:按年份分布、月度热力图、最长连续记录等。


8. 预览器兼容性修复

8.1 白屏诊断

本 App 在开发过程中经历了一次典型的"白屏"问题。构建成功(0 错误),但预览器显示空白。

诊断过程

Round 1: 构建成功 → 预览白屏
  → 怀疑 Set 不支持 → 移除所有 Set
  → 仍白屏

Round 2: 构建成功 → 预览白屏
  → 怀疑 for...of 不支持 → 移除所有 for...of
  → 仍白屏

Round 3: 构建成功 → 预览白屏  
  → 怀疑 Date() 静态初始化 → 替换为时间戳常量
  → 仍白屏

Round 4: 构建成功 → 预览白屏
  → 简化代码到最小可运行版本(188 行)
  → ✅ 预览成功

Round 5: 逐步添加功能
  → 确认每个功能的预览器兼容性

根本原因是多个因素的叠加:当代码同时使用了 Set、for…of、嵌套 ForEach、Date 静态初始化时,预览器无法正确处理。但单独移除任何一个,问题仍然存在。只有同时简化到最基础的结构,问题才消失。

8.2 预览器兼容清单

经过本 App 的排查,以下是 ArkTS Previewer 中已知的不完全支持的功能:

功能 支持情况 替代方案
Set / Map ⚠️ 部分支持 使用数组 + indexOf
for...of 迭代 ⚠️ 可能有问题 使用 for 循环 + 索引
顶层 new Date() ⚠️ 可能失败 使用时间戳常量
嵌套 ForEach ⚠️ 可能崩溃 减少嵌套层级
@Builderconst ❌ 不支持 内联表达式
Grid 组件 ⚠️ 部分版本有问题 使用 Column + Flex
Set.size ⚠️ 可能为 undefined 使用 Array.length

8.3 最简可行原则

最终版本的代码只有 188 行,是所有复杂功能移除后的最小子集。

移除的功能

  • 年份分组(嵌套 ForEach)
  • 年度统计柱状图(Grid)
  • 预置 12 条示例数据(减少为 3 条)
  • getYearGroups / getYearSummary 等辅助方法

保留的功能

  • 三 Tab 切换
  • 时间线卡片列表
  • 添加记忆弹窗
  • 记忆详情弹窗
  • 统计概览

这个"最简可行版本"原则不仅解决了预览器兼容性问题,也让代码更容易理解和维护。


9. 编译错误全记录

9.1 错误概览

本 App 共出现 8 个编译错误

# 错误代码 位置 原因 修复
1-2 10605074 L335, 341 Map 解构 const [y, c] of map 改为数组 + indexOf
3 10505001 L193 getYearGroups 返回类型不匹配 改为返回 number[]
4 10905209 L146 ForEach 中 const year = ... 改为内联
5-7 10905209 L279-283 ForEach 中 const year/count/maxCount 改为内联
8 10905209 L361 const mem = ... 改为内联

9.2 错误分类

10605074(解构):  2 个 → 25%
10505001(类型):  1 个 → 12.5%
10905209(Builder):5 个 → 62.5%

10905209 仍然是最常见的错误类型,占总数的 62.5%。从 App 1 到 App 28,这个错误从未缺席。

10605074(解构)在本 App 中出现了 2 次,都是因为使用了 Map 的 for (const [key, value] of map) 语法。替换为数组 + indexOf 后修复。

9.3 28 款 App 的错误趋势

App 1:   16  ← 初学
App 10:  11  ← 模式形成
App 20:   2  ← 高效期
App 24:  48  ← 新领域(AI 对话)
App 25:   3  ← 回归基线
App 26:   8  ← 全部同一类型
App 27:   5  ← 全部低级错误
App 28:   8  ← 包含 2 种类型

App 28 的错误分布呈现了典型的"稳定期模式":大部分是 10905209(5 个),小部分是新的语法错误(2 个 10605074 + 1 个 10505001)。

9.4 经验总结

经过 28 款 App 的实践,以下规则已经变成了"肌肉记忆":

已经不会犯的错误

  • 颜色常量忘加 interface ✅
  • ForEach key 函数作用域 ✅
  • 索引签名 ✅
  • 数字键名 ✅

偶尔还会犯的错误

  • @Builder 中写逻辑 ⚠️(第 28 次)
  • 解构赋值 ⚠️(第 2 次)
  • 方法返回类型不匹配 ⚠️(重构时的遗留)

10. 从复杂到极简的演进

10.1 版本对比

维度 初始版(479 行) 最终版(188 行) 变化
代码行数 479 188 -61%
文件体积 16.5 KB 6.2 KB -62%
@Builder 数量 7 个 6 个 -1
业务方法 11 个 2 个 -9
预览兼容性 ❌ 白屏 ✅ 正常 修复
编译错误 8 个 0 个 修复

代码精简的来源

功能移除:
  年份分组展示      → -60 行
  年度柱状图        → -40 行
  12 条预置数据     → -30 行
  getYearGroups()   → -20 行
  getYearSummary()  → -25 行
  getMaxYearCount() → -10 行

语法精简:
  const 移除(内联化)→ -15 行
  空白行压缩        → -20 行
  注释精简          → -15 行

总计减少:约 235 行

10.2 功能的取舍

为什么移除"年份分组"和"柱状图"?

年份分组:从用户角度来看,2024 年 → 回忆 1 → 回忆 2 → 2023 年 → 回忆 3 这种分组确实好看。但从技术角度来看,它需要一个嵌套 ForEach(ForEach 中套 ForEach),这是 ArkTS 预览器中最容易出问题的模式之一。

而且从产品角度来看,年份分组在数据量少的时候意义不大——只有 3 条记忆,分 2 个年份,每组最多 2 条,分组的视觉效果还不如直接按时间排列。只有数据量超过 20 条时,分组才开始展现价值。

年度柱状图:需要 Grid 组件 + 宽度百分比计算 + 多方法调用。对于"总条数"这个信息来说,柱状图的视觉价值大于信息价值。用户看到柱状图的第一反应是"哦,今年记了 3 条",而不是"这个杠的长度刚好是 30%"——他们只需要数字。

编辑和删除功能:初始版本包含了编辑和删除,但最终版本去掉了。原因有二:第一,编辑功能需要在弹窗中加载现有数据并允许修改,这增加了至少 30 行代码和一个新的 @State 变量;第二,对于一个"记录美好回忆"的 App,删除功能的存在暗示了"有些记忆不值得保留",这与产品理念相悖。

取舍原则:如果某个功能移除后,App 的核心价值(记录和浏览家庭记忆)不受影响,那就是可以移除的。

10.3 极简主义的十二字方针

本 App 的极简设计可以总结为十二个字:

看得见:打开 App 立刻看到时间线,不需要任何操作
记得快:点 Tab → 点中间 → 打字 → 保存,4 步完成记录
读得懂:每条卡片显示 emoji + 标题 + 年份 + 预览,不需要点进去就知道是什么

这十二个字对应了 App 的三个核心场景:浏览、记录、回顾。

看得见:时间线 Tab 是默认首页。打开 App 时,如果已经有记忆,直接展示卡片列表;如果没有,显示空状态引导文字。用户不需要学习任何操作就能看到内容。

记得快:从打开 App 到完成一条记忆的记录,最多 4 次点击。不需要选择日期(默认当前年)、不需要选择分类、不需要添加标签。标题写完后直接点保存,整个过程不超过 10 秒。

读得懂:每条卡片展示了四个信息维度——emoji(情绪)、标题(主题)、年份(时间)、内容摘要(预览)。用户在浏览时间线时,不需要点进详情就能了解每条记忆的大致内容。

10.4 极简主义的设计哲学

188 行代码的 App,能做的事:

  1. ✅ 浏览记忆(时间线)
  2. ✅ 添加记忆(标题最少)
  3. ✅ 查看详情(弹窗)
  4. ✅ 统计总条数
  5. ✅ 三 Tab 切换

不能做的事:

  1. ❌ 按年份分组浏览
  2. ❌ 柱状图统计
  3. ❌ 编辑/删除记忆
  4. ❌ 图片/视频附件
  5. ❌ 数据持久化

这些"不能做的事"中,有些是后续版本可以加的(编辑/删除/持久化),有些是设计上就不打算做的(图片/视频)。

关键认知:388 行的版本和 188 行的版本,核心价值相同。用户不会因为"少了一个柱状图"而觉得 App 不好用——他们只会因为"打开就能记、记完就能看"而觉得 App 顺手。


11. 第二十八款 App 全景回顾

11.1 数据总览

指标 数值
代码行数 188 行
编译错误数 8 个(修复后 0)
@State 变量 9 个
@Builder 方法 6 个
弹窗数 2 个
外部依赖 0 个
预置数据 3 条

11.2 二十八款 App 行数趋势

App 1:   767  ← 白噪音
App 8:   390  ← 情绪垃圾桶
App 16:  614  ← 梦境解析
App 24:  907  ← AI 树洞(峰值)
App 25:  488  ← 关怀平替
App 26:  508  ← 断段打卡
App 27:  340  ← 长辈说明书
App 28:  188  ← 记忆时光机(新低)

188 行 — 28 款 App 中代码量最少。打破了之前 App 27(长辈说明书,340 行)的记录。

这个下降趋势反映了两个事实:

  1. ArkUI 的开发效率在提升——同样功能用更少的代码实现
  2. 开发者对"什么功能是必要的"判断越来越清晰

11.3 二十八款 App 的核心发现

经过 28 款 App 的实践,以下是关于 ArkTS 开发的核心发现:

发现一:@Builder 中不能写逻辑 — 100% 遇到率
28 款 App,每一款都至少遇到一次 10905209 错误。这是 ArkTS 开发中唯一一个"一定会遇到"的错误。

发现二:预览器兼容性 — 最大坑
构建成功 ≠ 预览成功。预览器有自己的运行时限制,不支持的 API 会导致白屏,且错误信息不可见。

发现三:越简单的代码越稳定
188 行的 App 比 907 行的 App 更容易调试、更容易预览、更容易维护。

发现四:代码行数 ≠ 用户体验
188 行的 App 可以记录家庭记忆、浏览时间线、查看详情。907 行的 App 多了 4 个 AI 角色和 164 条回应模板——但核心交互(点击→看到内容)没有区别。


12. 结语

12.1 记忆的价值

「家庭记忆时光机」这款 App,技术上没有任何突破:没有 AI、没有动画、没有复杂的数据结构。只有 188 行代码、3 条预置记忆、2 个弹窗。

但它想传达的东西很简单:那些看起来普通的日常,回过头去看都是珍贵的记忆。

一起装饰圣诞树的下午、第一次看到大海的惊喜、全家人围在一起吃蛋糕的晚上——这些时刻在被记录的时候可能觉得"没什么特别的",但三年五年后翻出来看,心里会涌起一股暖意。

12.2 28 款 App 的旅程

App 1:  听说 ArkUI 是声明式的,试试
App 10: 原来 @Builder 不能写 let 啊
App 20: 28 篇博客,28 万字的记录
App 24: 48 个错误 — 那是探索的代价
App 28: 188 行 — 原来简单才是终点

从 907 行(AI 树洞)到 188 行(记忆时光机),这个旅程不仅是代码量的减少,更是对"什么是必要的"这个问题的持续追问。

AI 树洞的 907 行代码中,有 400 行是 AI 回应模板——这个功能对于 App 的核心价值(被倾听)是必要的。

记忆时光机的 188 行代码中,每一行都直接服务于核心价值(记录和浏览记忆)——没有"也许以后会用"的功能,没有"看起来更高级"的抽象。

12.3 给开发者的建议

  1. 从最小版本开始:先写 200 行能跑起来的版本,再考虑加功能。不要在一开始就想"万一用户需要这个怎么办"——你还没有用户
  2. 预览器不支持的 API:Set、Map、for…of、顶层 Date()——用数组和 for 循环替代。预览器是开发者的第一道防线,确保预览器能跑,模拟器和真机通常也不会有问题
  3. 一个功能一个测试:加一个新功能后立刻预览一下,不要等到加了 5 个功能再预览。如果全崩了,你根本不知道是哪个新功能引起的
  4. 188 行可以是完整的 App:不要觉得代码少就不够好。188 行可以记录家庭记忆、浏览时间线、查看详情——对用户来说,功能是完整的
  5. 记录比完美更重要:10 条朴素的记忆,比一个完美的空 App 更有温度。不要在"用什么颜色"、"用什么字体"上纠结太久,先让用户能记下第一条记忆
  6. 删除功能也是一种设计:加功能很容易,删功能需要勇气。但每次删掉一个不必要的功能,你的 App 就会变得更清晰、更稳定、更容易维护

12.4 二十八款 App 之后的思考

写到这里,这个系列已经覆盖了 28 款 App、28 篇博客。从第一款的"试试 ArkUI"到现在的"188 行记录家庭记忆",最深的感受是:

开发者的成长曲线不是加法,是减法。

刚开始的时候,觉得功能越多越好——Tab 越多越好、弹窗越多越好、动画越多越好。写了二十多款 App 之后,开始觉得"这个功能可以去掉"、“这个页面可以合并”、“这行代码可以删除”。

删除比添加更需要判断力。添加一个功能只需要知道"怎么实现",删除一个功能需要判断"真的需要吗"。这个判断力只能通过实践积累——没有捷径。

如果这个系列能给你一个启发,那就是:先写出来,再删到不能删。 Python 之禅里说"Simple is better than complex",在 ArkTS 开发中尤其如此。

12.5 感谢

28 款 App、28 篇博客、约 282,000 字。

从白噪音到记忆时光机,从 907 行到 188 行——这个系列见证了 ArkTS 的成熟,也见证了开发者的成长。

28 不是终点。如果你正在读这篇文章,也正在写你自己的 App 的路上——不管那是第 1 款还是第 100 款——记住:代码可以不断精简,但记录本身就有价值。

现在,打开 DevEco Studio,写下你的第 1 条记忆吧。


附录 A:核心代码速查

数据模型

interface Memory {
  id: number; date: number; year: number;
  emoji: string; title: string; content: string;
}

添加记忆

const mem: Memory = {
  id: Date.now(), date: Date.now(),
  year: this.newYear, emoji: this.newEmoji,
  title: this.newTitle.trim(), content: this.newContent.trim()
};
this.memories = [mem, ...this.memories];

时间线卡片

ForEach(this.memories, (mem: Memory) => {
  Column() {
    Text(mem.emoji).fontSize(28)
    Text(mem.title).fontSize(16).fontWeight(FontWeight.Bold)
    Text(mem.year + '年').fontSize(12)
    Text(mem.content).fontSize(13).maxLines(2)
  }.onClick(() => { this.showMemory = true; })
}, (mem: Memory) => mem.id.toString())

附录 B:色板

变量 用途
C.bg #F5EDE0 主背景(米黄)
C.bgCard #FFFAF0 卡片背景
C.primary #B8860B 主色(复古金)
C.primaryDim rgba(184,134,11,0.1) 光晕效果
C.text #3D2B1F 主文字(深棕)

附录 C:预览器兼容检查清单

初始化:
  □ 不使用 Set/Map(用数组替代)
  □ 顶层不使用 new Date()(用时间戳常量)
  □ 顶层不使用复杂计算

@Builder:
  □ 不使用 const/let 声明变量
  □ 不使用 for 循环
  □ 不使用 if-else 中的逻辑计算

ForEach:
  □ 始终提供 key 函数(第三个参数)
  □ 避免嵌套 ForEach

组件:
  □ 优先使用 Scroll + Column,而不是 Grid
  □ 弹窗使用 if 包裹 + Stack 层级

Logo

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

更多推荐