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

鸿蒙 Next 断网挑战营 App 开发实战:倒计时引擎 + 成就徽章系统 + 活动建议推荐

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


目录

  1. 引言
  2. 产品概念与数据模型
  3. 三 Tab 架构设计
  4. 实时计时器引擎
  5. 开始与结束挑战流程
  6. 成就徽章系统
  7. 挑战记录与历史列表
  8. 活动建议推荐机制
  9. 累计统计卡片
  10. 编译错误全记录
  11. 十七款 App 全景回顾
  12. 结语

1. 引言

1.1 数字戒断的必要性

现代人平均每天花在手机上的时间超过 5 小时。Notifications、短视频、社交媒体的即时反馈机制不断劫持我们的注意力。我们不是在"使用"手机,而是在被手机"使用"。

“断网挑战营"App 的核心理念很简单:放下手机,完成离线挑战。它不是一个"防沉迷"工具——不强制锁屏、不限制应用使用——而是一个自我记录和激励工具。你主动打开 App,点击"开始断网”,然后放下手机去做其他事。挑战时间自动累计,成就徽章逐步解锁,历史记录见证你的每一次离线。

与其说这是一个 App,不如说它是一个意念的锚点——每次看到计时器上的数字在增长,都是一次正向的心理暗示:“我在掌控我的注意力。”

1.2 本 App 的技术特色

断网挑战营是本系列第十七款 App,在技术上有几个显著特点。

首先,它引入了实时计时器引擎——基于 setInterval 每秒刷新,大号字体(42px)展示离线时长,营造仪式感。这是一个典型的"定时器 + UI 状态同步"模式,不同于之前的"一次性数据展示"类 App。

其次,它设计了成就徽章系统——10 个徽章按累计离线分钟数分级解锁,从"初试断网"(1 分钟)到"断网达人"(10000 分钟)。这是系列中首次引入游戏化的成就机制。

此外,本 App 新增了活动建议推荐系统——16 种线下活动的建议库,随机推荐 + 手动刷新。这个设计填补了用户"断网后不知道做什么"的空白。

1.3 第十七款 App 的系列数据

这是本系列的第十七款 App。

App 数量:    17
代码总行数:  ~11,200 行
编译错误数:  ~165 个
博客总字数:  ~180,000 字
技术博客数:  17 篇

2. 产品概念与数据模型

2.1 功能需求

用户故事 1:我想开始一次断网挑战,看到实时计时
用户故事 2:我想结束挑战,记录这次挑战的时长
用户故事 3:我想查看累计断网时间和历史成就
用户故事 4:我想通过挑战解锁成就徽章
用户故事 5:我想看到挑战历史记录
用户故事 6:我想获得离线活动建议

功能清单:
├── F1: 开始挑战(启动计时器 + 随机活动建议)
├── F2: 实时计时(setInterval 每秒刷新)
├── F3: 结束挑战(保存记录 + 检查徽章解锁)
├── F4: 累计统计(总时长 + 次数 + 成就数)
├── F5: 成就徽章(10 个里程碑分级解锁)
├── F6: 活动建议(16 种随机 + 手动刷新)
├── F7: 挑战记录(历史列表 + 完成/中断状态)
├── F8: 历史最佳展示
└── F9: 数据持久化(Preferences)

2.2 数据模型

interface ChallengeRecord {
  id: number;          // 唯一标识
  startTime: number;   // 开始时间戳
  endTime: number;     // 结束时间戳
  duration: number;    // 时长(分钟)
  completed: boolean;  // 是否完成
}

interface Badge {
  id: string;          // 徽章 ID
  name: string;        // 名称
  icon: string;        // Emoji 图标
  desc: string;        // 描述
  need: number;        // 累计分钟需求
  unlocked: boolean;   // 是否已解锁
}

ChallengeRecord 记录单次挑战的信息,Badge 则管理成就系统。两个模型之间通过 durationneed 字段关联——每次挑战结束后,累计所有记录的 duration,与每个 Badge 的 need 比较,决定是否解锁。

2.3 成就徽章设计

10 个徽章的解锁门槛从低到高排列,形成了一个"学习曲线":

徽章 图标 需求 心理效果
初试断网 🌱 1 分钟 几乎立即解锁,建立初始成就感
十分钟 10 分钟 第一次突破,完成即达标
半小时 🌙 30 分钟 需要一定坚持
一小时 60 分钟 第一个重要里程碑
两小时 🎯 120 分钟 中等难度
半天 ☀️ 360 分钟 需要持续参与
一天 🌈 720 分钟 高难度
三天 🔥 2160 分钟 资深用户
一周 🏆 4320 分钟 核心用户
断网达人 👑 10000 分钟 终极目标

门槛设计遵循"前易后难"的原则:前 3 个徽章在新用户第一次挑战后即可解锁,中期的 4-6 个需要每周几次的参与,最后的 3 个则是为持续使用数月的忠实用户准备的。


3. 三 Tab 架构设计

3.1 Tab 配置

buildTabContent() {
  if (this.activeTab === 0) this.buildChallengeTab()       // 挑战
  else if (this.activeTab === 1) this.buildAchievementTab() // 成就
  else this.buildHistoryTab()                               // 记录
}

三个 Tab 对应三种使用场景:

Tab 图标 功能 使用频率
挑战 计时器 + 统计 + 建议 每次打开
成就 🏆 徽章列表 + 进度 每周
记录 📋 历史挑战一览 偶尔

3.2 Tab 的设计意图

与其他 App 的"三 Tab 各自独立"不同,断网挑战营的三个 Tab 构成了一条用户路径

挑战 Tab(行动)→ 成就 Tab(反馈)→ 记录 Tab(复盘)

用户打开 App 时首先进入挑战 Tab(行动),开始或继续挑战;完成挑战后想查看自己的积累,切换到成就 Tab(反馈);偶尔想回顾过去的挑战,翻看记录 Tab(复盘)。这种"行动-反馈-复盘"的路径设计与习惯养成类产品的模式一致。


4. 实时计时器引擎

4.1 计时器实现

计时器是本 App 最核心的交互元素。使用 setInterval 每秒更新一次 elapsed 状态:

startTimer(): void {
  this.timerId = setInterval(() => {
    this.elapsed = Math.floor((Date.now() - this.startTime) / 1000);
  }, 1000);
}

elapsed 是一个 @State 变量,以为单位存储。UI 中的 formatDuration 方法将其转换为"时:分:秒"格式展示。

4.2 时间格式化

formatDuration(totalSec: number): string {
  let h = Math.floor(totalSec / 3600);
  let m = Math.floor((totalSec % 3600) / 60);
  let s = totalSec % 60;
  if (h > 0) {
    return h + '时 ' + m.toString().padStart(2, '0') + '分 ' + s.toString().padStart(2, '0') + '秒';
  }
  return m + '分 ' + s.toString().padStart(2, '0') + '秒';
}

当总时长超过 1 小时时,显示完整的小时-分钟-秒格式;不足 1 小时时,只显示分钟-秒。这种设计避免了"0 时 23 分 15 秒"这样浪费空间的显示。

4.3 计时器的生命周期管理

计时器在两种情况下被清理:

  1. 用户结束挑战stopChallenge() 中调用 stopTimer()
  2. App 进入后台或被销毁aboutToDisappear() 中调用 stopTimer()
aboutToDisappear(): void {
  this.stopTimer();
  this.saveData();
}

stopTimer(): void {
  if (this.timerId >= 0) {
    clearInterval(this.timerId);
    this.timerId = -1;
  }
}

aboutToDisappear 中的清理是必要的——如果用户没有主动结束挑战就关闭 App,计时器需要被清理以避免内存泄漏。

4.4 计时器 UI 状态机

计时器 UI 有两种状态:

状态 图标 标题 计时器 按钮
未开始 🏕️ “准备好断网了吗?” 00:00 “▶ 开始断网”(橙色)
进行中 🔥 “正在断网中…” 实时更新 “⏹ 结束挑战”(红色)

两个状态的切换通过 isActive 布尔值控制。开始按钮为橙色(C.primary),结束按钮为红色(C.fire),视觉上强化了"开始 = 积极,结束 = 警戒"的感知。


5. 开始与结束挑战流程

5.1 开始挑战

startChallenge(): void {
  this.isActive = true;
  this.startTime = Date.now();
  this.elapsed = 0;
  if (this.currentActivity === '') {
    this.currentActivity = ACTIVITIES[Math.floor(Math.random() * ACTIVITIES.length)];
  }
  this.startTimer();
}

开始挑战时同时做两件事:启动计时器 + 推荐活动建议。如果用户还没有活动建议(初始状态),随机推荐一个。

5.2 结束挑战

stopChallenge(): void {
  this.isActive = false;
  this.stopTimer();
  let endTime = Date.now();
  let durationMin = Math.round((endTime - this.startTime) / 60000);
  if (durationMin < 1) durationMin = 1;
  let record: ChallengeRecord = {
    id: Date.now(), startTime: this.startTime,
    endTime: endTime, duration: durationMin, completed: true
  };
  this.records = [record].concat(this.records);
  this.saveData();
  this.checkBadges();
}

结束挑战时有四个步骤:

  1. 停止计时器this.stopTimer()
  2. 计算时长Math.round((endTime - startTime) / 60000) 将毫秒转为分钟
  3. 保存记录:新记录插入数组头部(最新在前)
  4. 检查徽章:调用 checkBadges() 更新解锁状态

5.3 时长计算的精度

时长以分钟为单位(向下取整),而非秒。这意味着 30 秒的挑战会被记为 1 分钟。这个设计避免了用户"刷时长"的行为——计时器实时显示"多少分多少秒",但实际记录时按分钟计算,自动抹去不足 1 分钟的零头。

5.4 记录更新的响应式处理

this.records = [record].concat(this.records);

使用 concat 创建新数组,而非 push 修改原数组。在 ArkTS 中,@State 仅在新引用赋值时触发 UI 更新——push 不会触发重新渲染。


6. 成就徽章系统

6.1 徽章检查逻辑

checkBadges(): void {
  let total = this.getTotalMinutes();
  for (let i = 0; i < this.badges.length; i++) {
    if (!this.badges[i].unlocked && total >= this.badges[i].need) {
      this.badges[i].unlocked = true;
    }
  }
}

遍历所有徽章,对于未解锁且累计时长 >= 需求的徽章,将其 unlocked 设为 true。由于 badges 也是 @State 数组,赋值后 UI 自动更新。

6.2 累计时长计算

getTotalMinutes(): number {
  let total = 0;
  for (let r of this.records) total += r.duration;
  return total;
}

从所有挑战记录中累加时长。这个值在每次挑战结束后都可能变化,因此 checkBadges 每次都重新计算。

6.3 徽章的克隆问题

徽章列表的初始化涉及一个重要的 ArkTS 规则——不能使用展开运算符

// ❌ 错误:ArkTS 不支持展开运算符
this.badges = BADGE_LIST.map(b => ({ ...b }));

// ✅ 正确:显式复制每个属性
cloneBadge(b: Badge): Badge {
  return {
    id: b.id, name: b.name, icon: b.icon,
    desc: b.desc, need: b.need, unlocked: b.unlocked
  } as Badge;
}

BADGE_LIST 是模块级常量,其中的 unlocked 字段初始为 false。但 ArkTS 的模块级常量在多次操作 App 时可能会被意外修改(因为 @State badges 数组中的对象与常量引用同一对象)。通过 cloneBadge 显式复制每个属性,确保 badges 数组中的对象是独立的。

6.4 徽章列表的分组展示

成就 Tab 将徽章分为两组:已解锁和未解锁,分别用不同的 UI 展示:

✅ 已解锁 (3)
🌱 初试断网 - 完成首次断网挑战 ✅
⏰ 十分钟 - 累计断网10分钟      ✅
🌙 半小时 - 累计断网30分钟      ✅

🔒 未解锁 (7)
⭐ 一小时 - 累计断网60分钟      需要累计60分钟
🎯 两小时 - 累计断网120分钟     需要累计120分钟

已解锁的徽章不透明度为 1,未解锁的为 0.6。这种视觉区分让用户快速聚焦于"下一个要解锁的徽章"。


7. 挑战记录与历史列表

7.1 记录列表

历史 Tab 展示所有挑战记录,按时间倒序排列(最新在前):

ForEach(this.records, (r: ChallengeRecord) => {
  Column() {
    Row() {
      Text(r.completed ? '✅' : '⏹️')
      Column() {
        Text(this.formatDuration(r.duration))  // 时长
        Text(this.formatDate(r.startTime))       // 日期
      }
      Text(r.completed ? '完成' : '中断')
    }
  }
}, (r: ChallengeRecord) => r.id.toString())

每条记录包含:完成状态图标、时长、日期时间、完成/中断标签。

7.2 空状态

当没有记录时,显示空状态引导:

🏕️
还没有挑战记录
开始你的第一次断网挑战吧

7.3 日期格式化

formatDate(ts: number): string {
  let d = new Date(ts);
  let month = (d.getMonth() + 1).toString().padStart(2, '0');
  let day = d.getDate().toString().padStart(2, '0');
  return month + '/' + day + ' '
    + d.getHours().toString().padStart(2, '0') + ':'
    + d.getMinutes().toString().padStart(2, '0');
}

显示格式为 06/14 15:30,只显示月/日 时:分,不显示年份(因为大多数记录都在近期)。这种紧凑格式适合在列表卡片中展示。


8. 活动建议推荐机制

8.1 活动库

const ACTIVITIES: string[] = [
  '📖 读一本纸质书', '🚶 散步 15 分钟', '📝 写信给朋友', '🎨 画一幅画',
  '🧘 冥想 10 分钟', '🎵 听一张专辑', '🍳 做一顿饭', '🧹 整理房间',
  '📸 拍一组照片', '🌳 去公园坐坐', '☕ 泡一杯茶', '🧩 拼图',
  '🎹 弹奏乐器', '✍️ 练字', '🧶 做手工', '🏋️ 做运动',
];

16 种活动涵盖了阅读、创作、运动、家务、休闲等多个类别,确保不同兴趣的用户都能找到合适的建议。

8.2 随机推荐 + 手动刷新

// 随机推荐
this.currentActivity = ACTIVITIES[Math.floor(Math.random() * ACTIVITIES.length)];

// 手动刷新(按钮)
Text('🔄 换一个').onClick(() => {
  this.currentActivity = ACTIVITIES[Math.floor(Math.random() * ACTIVITIES.length)];
})

用户在开始挑战前可以看到一个活动建议和"换一个"按钮。不喜欢的建议可以随时刷新。

8.3 活动建议的时机

活动建议只在非活跃状态时显示。当计时器正在运行时(isActive === true),活动建议区域隐藏——因为在"断网"时看手机本身就是矛盾的。这个设计强化了 App 的核心理念:设定目标,放下手机,去做事


9. 累计统计卡片

9.1 三列统计

挑战 Tab 的顶部在计时器下方展示了三个统计指标:

📊 累计     🏆 成就     📋 挑战
120分钟     3/10        15次

使用 buildStatItem Builder 实现:

@Builder
buildStatItem(icon: string, label: string, value: string) {
  Column() {
    Text(icon).fontSize(22)
    Text(value).fontSize(16).fontColor(C.text).fontWeight(FontWeight.Bold)
    Text(label).fontSize(11).fontColor(C.textLight)
  }.width('30%').padding(10).backgroundColor(C.cardBg).borderRadius(14)
}

三个卡片等宽(30%),间距由外层 Row 的 margin 控制。这个三列布局简洁有效,在系列多个 App 中被使用。

9.2 历史最佳

在活动建议下方,如果存在历史记录,展示单次最佳时长:

🏅 历史最佳:45 分 30 秒

这个指标与累计时长不同——它展示的是"单次挑战的最佳表现",激励用户尝试打破自己的记录。

9.3 数据计算的性能

getTotalMinutes(): number {
  let total = 0;
  for (let r of this.records) total += r.duration;
  return total;
}

getBestDuration(): number {
  let best = 0;
  for (let r of this.records) {
    if (r.duration > best) best = r.duration;
  }
  return best * 60; // 转为秒用于 formatDuration
}

这两个方法在每次 UI 刷新时都会被调用(因为它们用在 @Builder 中)。对于几十到几百条记录来说,遍历全部记录计算总和的性能开销可以忽略不计。


10. 编译错误全记录

10.1 错误概览

本 App 出现 3 个编译错误

# 错误类型 位置 根因
1 展开运算符 aboutToAppear 第 77 行 { ...b } 在 ArkTS 中不被支持
2 属性不存在 buildAchievementTab 第 205 行 C.success 未在 ColorScheme 中定义
3 @Builder 中 let buildChallengeTab / buildAchievementTab let best / let unlocked, locked

10.2 关键错误:展开运算符

现象BADGE_LIST.map(b => ({ ...b })) 报错 “It is possible to spread only arrays or classes derived from arrays into the rest parameter or array literals”。

根因:ArkTS 的展开运算符(spread operator)限制比标准 TypeScript 严格——只能用于展开数组到参数或数组字面量中,不能用于对象字面量。

// ❌ 错误:对象展开
{ ...b }

// ✅ 正确:显式复制属性
{ id: b.id, name: b.name, icon: b.icon, desc: b.desc, need: b.need, unlocked: b.unlocked }

这个限制在系列第十二款 App(家庭大富翁)中已经遇到过。当时的教训是"展开运算符替代"。但在本 App 中,我又一次犯了同样的错误。

教训:即使是已知错误,也可能会重复犯。关键是修复速度——第一次遇到花了 3 个轮次,这次只花了 1 个轮次。

10.3 关键错误:属性不存在

现象C.success 报错 “Property ‘success’ does not exist on type ‘ColorScheme’”。

根因:ColorScheme 接口中没有定义 success 属性。在 ArkTS 中,对象的属性和类型必须严格匹配——接口中有哪些属性,常量中就必须有那些属性,不能多也不能少。

这个错误在系列前作中已经很少出现了(因为每次新建 App 时,都是复制上一个 App 的 ColorScheme 然后修改),但本 App 是从新接口定义开始写的,忘了在接口中加入 success

10.4 关键错误:@Builder 中的 let

现象let best = this.getBestDuration()let unlocked = this.badges.filter(...) 报错 “Only UI component syntax can be written here”。

根因:这是 ArkTS 中最常见的错误类型,在本系列中占 36%。在 @Builder 方法中,不能有任何形式的变量声明。

本 App 中犯了两次:

// 错误 1:在 buildChallengeTab 中
let best = this.getBestDuration();  // ❌

// 错误 2-3:在 buildAchievementTab 中
let unlocked = this.badges.filter(b => b.unlocked);  // ❌
let locked = this.badges.filter(b => !b.unlocked);   // ❌

修复方式都是在 @Builder 中直接用方法调用替代变量:

// ✅ 修复 1:内联调用
if (this.getBestDuration() > 0) { Text(...) }

// ✅ 修复 2-3:提取为独立方法 + Builder
this.buildBadgeSection('✅ 已解锁', this.getUnlockedBadges(), true)

10.5 十七款 App 错误数趋势

22 → 17 → 16 → 1 → 12 → 12 → 10 → 4 → 11 → 11 → 3 → 8 → 7 → 12 → 1 → 4 → 3

第十七款 App 的错误数为 3 个,处于历史低位。

10.6 错误恢复速度

本 App 的 3 个错误在 1 个轮次内全部修复。对比第一款的 22 个错误需要 5 个轮次,修复效率提升了 5 倍以上。这个效率提升来自:

  1. 错误模式已知:看到错误信息能立即知道是"@Builder 中的 let"还是"属性不存在"
  2. 修复模式已验证:let → 提取方法,属性缺失 → 加接口字段,展开 → 显式复制
  3. 工具辅助:通过 build log 快速定位错误行号

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 工具

11.2 行为养成类 App 的设计模式

断网挑战营属于"行为养成"类 App,与"宠物日记"(第七款)在模式上有相似之处。行为养成类 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 断网挑战营 已知错误也会重复犯

11.4 关于"重复犯错"的思考

第 17 条教训"已知错误也会重复犯"是系列中首次出现的关于"开发者行为"的教训——之前的教训都关于"ArkTS 语法"。

为什么已知错误会重复犯?

原因 1:惯性思维。在标准 TypeScript 中,{ ...b } 是最自然的对象克隆方式。写了十几年代码形成的肌肉记忆,不会因为换了语言就自动消失。

原因 2:上下文切换。从上一个 App(梦境解析)切换到断网挑战营时,注意力在功能设计上,而不是语法规则上。

原因 3:压力因素。时间紧、任务多时,大脑会自动切换到"最熟悉的写法",而不是"这个语言允许的写法"。

解决方案不是"永远不会再犯",而是缩短从犯错到修复的时间。从一个轮次到修复到预览通过,从第一次的 3 轮次到这次的 1 轮次——证明了"免疫系统"的有效性。


12. 结语

12.1 十七款 App 的开发历程

App1  🎵  白噪音          → 初识 ArkUI
App2  ⏳  时间胶囊        → 数据持久化
App3  🧊  冰箱剩菜        → Tab 架构
App4  😅  尴尬粉碎机      → 模式复用
App5  🛡️  防骗训练        → 适老化
App6  💡  碎片学习        → 学习激励
App7  🐶  宠物日记        → 紧凑风格
App8  🗑️  情绪垃圾桶      → 情感交互
App9  🧭  线下寻宝        → 社交互动
App10 🗡️  订阅刺客        → 暗色主题
App11 🎑  声音明信片      → 模拟录音
App12 🎲  家庭大富翁      → 回合制游戏
App13 📚  二手书漂流瓶    → 随机匹配
App14 🧹  废话过滤器      → 自然语言检测
App15 🌱  绿植领养        → 缘分匹配
App16 🌙  梦境解析        → 潜意识探索
App17 🏕️  断网挑战营      → 行为养成

12.2 计时器类 App 的关键设计

断网挑战营是系列中第一款涉及实时计时器的 App。计时器类 App 有三个关键设计点:

  1. 生命周期管理:计时器必须在 App 销毁时被清理,否则会导致内存泄漏。aboutToDisappear 中的 clearInterval 是必须的。

  2. 响应式更新:计时器每秒更新 UI,必须使用 @State 变量来触发 ArkUI 的重新渲染。普通变量不会触发 UI 更新。

  3. 精度取舍:展示用秒(实时感强),记录用分钟(减少数据噪点)。两种精度的配合既满足了用户的"实时反馈"需求,又避免了"刷时长"的问题。

12.3 ArkUI 的终极评价(第五次修订)

经过十七款 App 的实践,ArkUI 的评价已经趋于稳定。

优势(与前作一致):

  • 声明式 DSL + @State 响应式
  • 编译检查有效
  • Preferences API 简洁

不足(与前作一致):

  • @Builder 语法约束过严
  • 展开运算符限制
  • 部分 API 与 Web CSS 差异大

最新发现:最大的风险已经不再是语法规则本身,而是开发者行为——即使熟悉所有规则,在疲劳或赶时间时还是会犯已知错误。

12.4 给开发者的建议

  1. 行为养成类 App 的设计模板:核心记录 → 累计统计 → 成就激励 → 历史回顾。这个模板可以复用到习惯追踪、学习打卡、健身记录等多个场景。

  2. 计时器记得清理:setInterval/setTimeout 的清理代码放在 aboutToDisappear 中,这是一种安全的默认做法。

  3. 重复犯错不可怕:真正重要的是修复速度。建立自己的"错误免疫系统"——哪些错误最常见、怎么最快修复、怎么预防下一次。

  4. App 也可以是一个意念锚点:不是所有 App 都要提供复杂的"功能"。有时候,一个计时器 + 几个成就徽章,就足以帮助用户建立积极的习惯。

12.5 感谢与展望

十七款 App、十七篇博客、约 180,000 字——从 6 月 13 日到 6 月 14 日,完成了全部 App 开发和博客撰写。

错误数从 22 降到 3,下降了 86%。但比错误数更重要的是修复错误的速度——从第一款的 5 轮次到第十七款的 1 轮次。

现在,打开 DevEco Studio,去创造属于你自己的 App 吧。


附录 A:第十七款 App 核心代码

实时计时器

startTimer(): void {
  this.timerId = setInterval(() => {
    this.elapsed = Math.floor((Date.now() - this.startTime) / 1000);
  }, 1000);
}

stopTimer(): void {
  if (this.timerId >= 0) {
    clearInterval(this.timerId); this.timerId = -1;
  }
}

开始与结束挑战

startChallenge(): void {
  this.isActive = true; this.startTime = Date.now(); this.elapsed = 0;
  this.startTimer();
}

stopChallenge(): void {
  this.isActive = false; this.stopTimer();
  let durationMin = Math.round((Date.now() - this.startTime) / 60000);
  if (durationMin < 1) durationMin = 1;
  this.records = [{ id: Date.now(), startTime: this.startTime,
    endTime: Date.now(), duration: durationMin, completed: true
  } as ChallengeRecord].concat(this.records);
  this.saveData();
  this.checkBadges();
}

成就检查

checkBadges(): void {
  let total = this.getTotalMinutes();
  for (let i = 0; i < this.badges.length; i++) {
    if (!this.badges[i].unlocked && total >= this.badges[i].need) {
      this.badges[i].unlocked = true;
    }
  }
}

徽章克隆

cloneBadge(b: Badge): Badge {
  return { id: b.id, name: b.name, icon: b.icon,
    desc: b.desc, need: b.need, unlocked: b.unlocked } as Badge;
}

附录 B:系列速查

指标 数值
App 数量 17
博客总字数 ~180,000 字
代码总行数 ~11,200 行
编译错误 ~165 个
@Builder 方法 ~225 个
修复轮次 33 轮

Logo

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

更多推荐