鸿蒙 Next 绿植领养 App 开发实战:缘分匹配机制 + 双视角数据 + 养护难度分级



鸿蒙 Next 绿植领养 App 开发实战:缘分匹配机制 + 双视角数据 + 养护难度分级
作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio
语言框架:ArkTS + ArkUI
字数:约 9800 字
目录
- 引言
- 产品概念与数据模型
- 三 Tab 架构设计
- 花圃列表与筛选机制
- 缘分匹配引擎
- 送养与领养流程
- 养护难度分级系统
- 双视角数据展示
- 编译错误全记录
- 十五款 App 全景回顾
- ArkUI 开发经验再再总结
- 结语
1. 引言
1.1 闲置绿植的困境
在城市化进程加速的今天,越来越多人选择在家中或办公室摆放绿植。绿植不仅美化环境,还能净化空气、缓解压力。然而,绿植市场存在一个普遍问题:植物买回来容易,养下去难。搬家、出差、养护不当或单纯想换新品种,都可能导致大量健康绿植被闲置甚至丢弃。
据统计,每个城市家庭平均拥有 3-5 盆绿植,其中约 30% 因无人照料或搬家而被遗弃。与此同时,很多人想养绿植却不知从何开始——买贵的怕养死,买便宜的又缺乏选择。
"绿植领养"App 将"领养代替购买"的公益理念引入绿植领域。用户可以将闲置的绿植拍照上传,标注养护难度和植物信息;其他用户通过"缘分匹配"或浏览花圃,找到心仪的绿植并领养回家。每一盆绿植的流转,都是一次生命的延续。
1.2 本 App 的技术特色
本 App 在技术上与"二手书漂流瓶"App 有相似之处(都是"送-匹配-领"的三段式社交),但也引入了几个新的技术点。
首先,养护难度分级系统是本 App 的特色之一。每盆绿植标记为"🟢 容易"“🟡 中等”"🔴 较难"三个等级,并通过独立的选择器弹窗让用户选择。这个系统使用了 ForEach + 选中态高亮的 Picker 模式,为后续 App 提供了一个通用的"单选列表"组件模板。
其次,缘分匹配机制在随机匹配的基础上加入了状态机设计——匹配前显示"🎍 点击匹配",匹配后切换为详细展示,包括植物信息卡片和"领养/换一盆"操作按钮。
此外,本 App 在编译过程中零编译错误(初版开发),这得益于系列前作积累的模式复用。但后续在删除 @Builder 中的 let 声明时经历了一个修复轮次。
1.3 第十五款 App 的系列数据
这是本系列的第十五款 App。
App 数量: 15
代码总行数: ~10,200 行
编译错误数: ~148 个
博客总字数: ~160,000 字
技术博客数: 15 篇
2. 产品概念与数据模型
2.1 功能需求
用户故事 1:我想把闲置的绿植送养给需要的人
用户故事 2:我想浏览所有待领养的绿植
用户故事 3:我想通过缘分匹配随机邂逅一盆绿植
用户故事 4:我想了解每盆绿植的养护难度
用户故事 5:我想领养一盆心仪的绿植
用户故事 6:我想查看我送养和领养的记录
功能清单:
├── F1: 送养绿植(名称 + 品种 + 分类 + 养护难度 + 描述 + 地点 + 昵称)
├── F2: 花圃浏览(全部绿植列表,带送养/已领养状态)
├── F3: 筛选切换(全部/待领养两种视图)
├── F4: 缘分匹配(随机抽取一盆待领养绿植)
├── F5: 领养流程(输入昵称后领养)
├── F6: 养护难度选择器(三选一弹窗)
├── F7: 10 分类 Grid 选择器(5 列布局)
├── F8: 我的记录(送养/领养双视角)
└── F9: 数据持久化(Preferences)
2.2 数据模型
interface Plant {
id: number; // 唯一标识
name: string; // 植物名称
species: string; // 品种(如:绿萝、虎皮兰)
category: string; // 分类(观叶/多肉/开花…)
difficulty: string; // 养护难度(🟢容易/🟡中等/🔴较难)
description: string; // 描述或养护须知
location: string; // 所在地
giver: string; // 送养人昵称
date: number; // 送养日期时间戳
isAdopted: boolean; // 是否已被领养
adopter: string; // 领养人昵称
}
与图书漂流瓶的数据模型相比,本 App 新增了两个字段:species(品种)和 difficulty(养护难度)。前者让绿植信息更具体,后者帮助领养人判断是否适合自己。双视角设计的核心仍然是 giver 和 adopter 两个字段——通过昵称匹配实现"我送养的"和"我领养的"两个视图。
2.3 分类体系
const CATEGORIES: string[] = ['观叶', '多肉', '开花', '绿萝', '仙人掌', '香草', '水培', '蕨类', '藤本', '果树'];
const CAT_ICONS: string[] = ['🌿', '🌵', '🌺', '🌱', '🌵', '🌿', '💧', '🍃', '🌿', '🍎'];
10 个分类使用 5 列 Grid 展示,每个分类有对应的 Emoji 图标。注意有些分类使用了相似的 Emoji(如观叶和香草都是 🌿),但这不影响功能——Emoji 只是视觉辅助,分类文本才是关键标识。
2.4 养护难度分级
const DIFFICULTIES: string[] = ['🟢 容易', '🟡 中等', '🔴 较难'];
三个级别的设计考虑了不同领养人的需求:
- 🟢 容易:适合新手,几乎不需要特殊照料(如绿萝、虎皮兰)
- 🟡 中等:需要一定的养护知识(如多肉、吊兰)
- 🔴 较难:需要专业养护经验(如兰花、盆景)
难度分级通过独立的选择器弹窗设置,与分类选择器并列。
3. 三 Tab 架构设计
3.1 Tab 配置
App 采用经典的三 Tab 架构:
buildTabContent() {
if (this.activeTab === 0) this.buildGarden() // 花圃
else if (this.activeTab === 1) this.buildAdoptPage() // 领养
else this.buildMyPage() // 我的
}
三个 Tab 覆盖了 App 的三大核心功能:
| Tab | 图标 | 功能 | 用户意图 |
|---|---|---|---|
| 花圃 | 🌿 | 浏览全部绿植列表 | 我想看看有什么植物 |
| 领养 | 🏡 | 缘分匹配随机抽取 | 帮我选一盆 |
| 我的 | 👤 | 送养/领养双视角记录 | 我的绿植去哪了 |
3.2 Tab 栏实现
Tab 栏使用 position + translate 固定到底部:
buildTabBar() {
Row() {
this.buildTabItem(0, '🌿', '花圃')
this.buildTabItem(1, '🏡', '领养')
this.buildTabItem(2, '👤', '我的')
}.width('100%').height(56).backgroundColor(C.cardBg)
.borderRadius({ topLeft: 20, topRight: 20 })
.shadow({ radius: 12, color: 'rgba(0,0,0,0.06)', offsetY: -3 })
.padding({ left: 8, right: 8 })
.justifyContent(FlexAlign.SpaceAround)
.position({ x: 0, y: '100%' }).translate({ y: -56 })
}
这种实现在系列中已经使用过 14 次,是一个经过充分验证的模式。
3.3 头部操作区
头部右侧的"送养"按钮只在花圃 Tab(activeTab === 0)时显示。这个设计避免了在其他 Tab 中误触:
if (this.activeTab === 0) {
Row() {
Text('➕')
Text('送养')
}.onClick(() => { this.openAdd(); })
}
4. 花圃列表与筛选机制
4.1 列表渲染
花圃 Tab 使用 ForEach 展示绿植列表。每盆植物以卡片形式展示分类图标、名称、品种、送养人、分类标签、养护难度和领养状态:
ForEach(this.getFilteredList(), (p: Plant) => {
Column() {
Row() {
// 分类图标色块
Column() { Text(CAT_ICONS[CATEGORIES.indexOf(p.category)]) }
.width(48).height(48)
.backgroundColor(p.isAdopted ? C.border + '44' : C.primary + '15')
.borderRadius(12)
// 信息区
Column() {
Text(p.name)
Text(p.species + ' · ' + p.giver)
// 分类标签 + 养护难度 + 领养状态
Row() {
Text(p.category)
Text(p.difficulty)
if (p.isAdopted) Text('✅ 已领养')
else Text('🏡 待领养')
}
}
}
}
.opacity(p.isAdopted ? 0.55 : 1.0)
.onClick(() => { /* 打开详情 */ })
})
4.2 状态视觉区分
与图书漂流瓶类似,本 App 使用三种视觉提示区分领养状态:
- 待领养:图标色块为绿色半透明背景,整卡不透明,状态标签为绿色"🏡 待领养"
- 已领养:图标色块为灰色背景,整卡 55% 半透明,状态标签为绿色"✅ 已领养"
4.3 筛选切换
花圃头部右侧有一个筛选切换按钮,在"全部"和"待领养"两种视图间切换:
Row() {
Text(this.filterAdopted ? '✅ 全部' : '🔄 待领养')
.onClick(() => { this.filterAdopted = !this.filterAdopted; })
}
filterAdopted 是一个布尔状态。当为 true 时显示全部绿植(包括已领养的),当为 false 时只显示待领养的:
getFilteredList(): Plant[] {
if (this.filterAdopted) return this.list;
return this.list.filter(p => !p.isAdopted);
}
4.4 空状态
当没有绿植时显示空状态提示:
🌱
还没有绿植
点击"送养"让闲置的绿植找到新家
5. 缘分匹配引擎
5.1 核心逻辑
领养 Tab 是本 App 的特色功能。用户点击"匹配缘分"按钮,系统从所有待领养的绿植中随机选择一盆展示:
pickSurprise(): void {
let available = this.list.filter(p => !p.isAdopted);
if (available.length === 0) return;
this.surpriseAnim = true;
this.surprisePlant = available[Math.floor(Math.random() * available.length)];
}
这个实现与图书漂流瓶的随机匹配机制完全一致:先 filter 筛选可用列表,再用 Math.random() 计算随机索引。但它增加了一个状态管理的细节——surpriseAnim 标志位用于切换展示状态。
5.2 状态机设计
缘分匹配的 UI 逻辑可以描述为一个三状态有限状态机:
| 状态 | 触发条件 | 展示内容 |
|---|---|---|
| S0: 初始 | 页面加载/无匹配 | 🎍 + “点击匹配一盆绿植” + "匹配缘分"按钮 |
| S1: 空池 | 没有待领养绿植 | 🌱 + “暂时没有待领养的绿植” |
| S2: 已匹配 | 用户点击匹配 | 🌿 + 植物卡片 + "领养/换一盆"按钮 |
if (可用列表为空) {
// S1: 空状态
Text('暂时没有待领养的绿植')
} else {
// S0 或 S2
Text(this.surpriseAnim ? '🌿' : '🎍')
Text(this.surpriseAnim ? '缘分到了!' : '点击匹配一盆绿植')
if (this.surprisePlant) {
// S2: 展示匹配结果
Text(植物名称 + 品种 + 分类 + 难度 + 送养人)
Text('我要领养') // → 领养弹窗
Text('换一盆') // → 重新匹配
} else {
// S0: 展示匹配按钮
Text('匹配缘分')
}
}
5.3 与图书漂流瓶的对比
图书漂流瓶 App 也有类似的随机匹配功能(“捞瓶子”),但两者有两个关键区别:
-
匹配后操作:图书漂流瓶匹配后只能"查看详情"或"换一本",领养操作在详情页完成。绿植领养在匹配卡片上直接提供了"我要领养"按钮,缩短了操作路径。
-
空状态位置:图书漂流瓶的空状态在卡片位置,绿植领养的空状态占据了整个 Tab 页面的中心位置,视觉上更突出。
这些差异反映了两个 App 的使用场景不同:图书漂流瓶偏"探索"(用户打开多次捞瓶子),绿植领养偏"决策"(用户打开一次领养一盆)。
6. 送养与领养流程
6.1 送养表单
送养弹窗包含 7 个输入字段,是系列中字段最多的表单之一:
| 字段 | 组件 | 必填 | 默认值 |
|---|---|---|---|
| 昵称 | TextInput | ✅ | - |
| 植物名称 | TextInput | ✅ | - |
| 品种 | TextInput | ❌ | “未知品种” |
| 分类 | Grid Picker | ❌ | 第一项 |
| 养护难度 | List Picker | ❌ | 第一项 |
| 地点 | TextInput | ❌ | “未知地点” |
| 描述 | TextArea | ❌ | 空 |
相比图书漂流瓶,本 App 新增了"品种"和"养护难度"两个字段。
6.2 品种字段的设计
“品种"字段(species)是一个巧妙的 UX 设计:它介于"名称"和"分类"之间,弥补了这两个字段的信息空白。用户知道一盆植物叫"小绿”(名称),但不知道它是什么植物——品种字段可以填"绿萝",这样领养人就能了解植物的具体种类。
名称:小绿
品种:绿萝 ← 补充信息
分类:观叶
6.3 领养流程
用户从详情页或匹配卡片点击"我要领养"后弹出领养弹窗:
doAdopt(): void {
if (this.adopterName.trim() === '' || this.selected === null) return;
let p = this.selected as Plant;
p.isAdopted = true;
p.adopter = this.adopterName.trim();
this.list = this.list.concat([]);
this.showAdopt = false;
this.selected = null;
this.saveData();
}
注意 this.list = this.list.concat([]) 这个模式——通过 concat 创建一个新的数组引用,触发 @State 的响应式更新。不直接修改原数组,这是 ArkTS 的推荐做法。
领养后,该植物的 isAdopted 变为 true,adopter 记录领养人昵称。此后其他用户将无法再次领养,花圃列表中该植物的状态也相应更新。
6.4 弹窗的层级设计
送养弹窗(buildAddDialog)的 y 坐标设置为 6%,比前作的 8% 更靠上。这是因为送养表单有 7 个字段,需要更多的垂直空间。详情弹窗的 y 坐标设置为 14%,领养弹窗为 26%——这些数值都是根据弹窗内容高度精心调整的。
弹窗全部放在 Stack 的根层级渲染,使用 if 条件包裹:
if (this.showAdd) this.buildAddDialog()
if (this.showDetail && this.selected) this.buildDetailDialog()
if (this.showAdopt && this.selected) this.buildAdoptDialog()
if (this.showCatPicker) this.buildCatPicker()
if (this.showDiffPicker) this.buildDiffPicker()
这种设计确保了每个弹窗都不受 Column 布局的约束。
7. 养护难度分级系统
7.1 难度选择器设计
养护难度选择器是一个独立的选择弹窗,使用列表形式展示三个难度选项:
@Builder
buildDiffPicker() {
Column() {
// 半透明遮罩
Column().backgroundColor('rgba(27,58,27,0.4)')
.onClick(() => { this.showDiffPicker = false; })
// 弹窗内容
Column() {
Text('选择养护难度')
ForEach(DIFFICULTIES, (d: string, idx: number) => {
Row() {
Text(d)
if (this.newDifficulty === idx) Text(' ✓')
}
.backgroundColor(this.newDifficulty === idx ? C.primary + '10' : 'transparent')
.onClick(() => { this.newDifficulty = idx; this.showDiffPicker = false; })
}, (d: string) => d)
}
}
}
7.2 选中态设计
每个选项用两个视觉提示标识选中态:
- 选中时字体颜色变为绿色(
C.primary)+ 加粗 - 选中时背景变为绿色 6% 透明
- 选中行右侧显示 ✓ 标记
Text(d).fontColor(this.newDifficulty === idx ? C.primary : C.text)
.fontWeight(this.newDifficulty === idx ? FontWeight.Bold : FontWeight.Normal)
if (this.newDifficulty === idx) Text(' ✓')
7.3 两个选择器的对比
本 App 有两个选择器弹窗:
| 选择器 | 布局 | 列数 | 选项数 | 宽度 |
|---|---|---|---|---|
| 分类选择器 | Grid(5 列) | 5 | 10 | 85% |
| 难度选择器 | List(1 列) | 1 | 3 | 75% |
两者采用了不同的布局策略:分类选项多,使用 Grid 节省空间;难度选项少,使用 List 让每个选项更突出。
8. 双视角数据展示
8.1 我的 Tab
“我的” Tab 使用昵称进行双视角数据筛选,这与图书漂流瓶的设计一致:
getGaveList(): Plant[] {
return this.list.filter(p => p.giver === this.newGiver || this.newGiver === '');
}
getAdoptedList(): Plant[] {
return this.list.filter(p => p.adopter === this.newGiver && this.newGiver !== '');
}
两个列表的筛选逻辑略有不同:
- “我送养的”:当用户未输入昵称时(
this.newGiver === ''),返回全部 - “我领养的”:要求昵称不为空,确保数据准确
8.2 Builder 复用
两个列表共享同一个 buildSectionList Builder:
@Builder
buildSectionList(title: string, plants: Plant[]) {
if (plants.length > 0) {
Text(title + ' (' + plants.length + ')')
ForEach(plants, (p: Plant) => {
Row() { /* 植物卡片 */ }
})
}
}
这个方法在最初的实现中使用了两个独立的代码块(一个 for 送养列表,一个 for 领养列表),导致代码冗余且包含 let 声明。重构后合并为一个 Builder + 参数化的方案,不仅减少了代码量,还解决了编译错误。
8.3 昵称引导
如果用户还未输入昵称,"我的"页面顶部显示引导提示:
请先在"送养"时输入你的昵称
这个引导与图书漂流瓶的设计一致。
9. 编译错误全记录
9.1 错误概览
本 App 在初版开发时 零编译错误——这是系列中第二款零错误的 App(第一款是尴尬粉碎机)。但在后续重构中出现了 1 个编译错误,合计 1 个错误。
| # | 错误类型 | 位置 | 根因 |
|---|---|---|---|
| 1 | @Builder 中 let | buildMyPage | 声明式 UI 中不能使用 let 变量 |
9.2 零编译错误的秘密
绿植领养的初版开发能够实现零编译错误,主要得益于以下三点:
1. 模式复用:本 App 的整体架构与图书漂流瓶几乎一致——三 Tab、弹窗、ForEach 列表、双视角数据。有了前 13 款 App 的经验积累,"应该怎么做"已经变成了肌肉记忆。
2. 初期代码简洁:初版代码直接将 let 用在 @Builder 中(导致 1 个错误),但在系列前作的经验下,所有其他 Builder 中都避免了使用 let。
3. 紧凑风格:每个 Builder 方法控制在 50 行以内,逻辑放在普通方法中,UI 保持在 Builder 中。这种"厚逻辑薄 UI"的模式在系列第 7 款 App(宠物日记)中确立,经过 8 款 App 的验证已经成为标准开发模式。
9.3 修复过程
第 1 个错误出现在对 buildMyPage 进行重构时——将列表渲染抽离为 Builder 方法时引入了 let。修复方式是将过滤逻辑移到普通方法中:
// ❌ 错误:@Builder 中使用 let
@Builder
buildMyPage() {
let gaveList = this.list.filter(...); // 编译错误
let adoptedList = this.list.filter(...); // 编译错误
}
// ✅ 正确:过滤逻辑移到普通方法
getGaveList(): Plant[] { return this.list.filter(...); }
getAdoptedList(): Plant[] { return this.list.filter(...); }
@Builder
buildMyPage() {
this.buildSectionList('🌱 我送养的', this.getGaveList())
this.buildSectionList('🏡 我领养的', this.getAdoptedList())
}
9.4 十五款 App 错误数趋势
22 → 17 → 16 → 1 → 12 → 12 → 10 → 4 → 11 → 11 → 3 → 8 → 7 → 12 → 1
第十五款 App(绿色柱)是一个新的低点——1 个错误,与第四款"尴尬粉碎机"持平。这个数据说明模式复用和紧凑风格确实有效。
9.5 错误的分类
如果将"零编译错误初版"和"重构引入错误"分开统计:
- 初版开发:0 个错误
- 重构引入:1 个错误(@Builder 中的 let)
- 总计:1 个错误
这意味着如果对需求有清晰的理解、对架构有成熟的模式,完全可以做到一次性通过 ArkTS 编译器。每一次重构、每一次代码优化,反而可能引入新的错误。
10. 十五款 App 全景回顾
10.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 | 社交 |
10.2 社交类 App 对比
本系列共有 3 款社交类 App:
| App | 核心机制 | 数据模型 | 双视角 |
|---|---|---|---|
| 🧭 线下寻宝 | 藏宝 + 寻宝 | location + hint | 藏者/寻者 |
| 📚 二手书漂流瓶 | 放漂 + 捞瓶子 | book + giver/claimer | 放漂人/认领人 |
| 🌱 绿植领养 | 送养 + 缘分匹配 | plant + giver/adopter | 送养人/领养人 |
三款社交 App 的共同点是:数据在用户之间流转,数据模型包含"发起方"和"接收方"两个视角。这种设计模式非常适合 C2C 类型的应用。
10.3 错误类型终极统计
十五款 App 共约 148 个编译错误,分布如下:
| 错误类型 | 数量 | 占比 |
|---|---|---|
| @Builder 语法(let/return/闭包) | 53 | 36% |
| 对象字面量无类型 | 14 | 9% |
| 属性不存在/拼写错误 | 18 | 12% |
| 展开运算符 | 6 | 4% |
| 级联错误 | 24 | 16% |
| Text 组件限制 | 3 | 2% |
| BorderOptions 语法 | 2 | 1% |
| 渲染层级问题 | 2 | 1% |
| @Builder 注解缺失 | 1 | 1% |
| 新引入(重构) | 1 | 1% |
| 其他 | 24 | 16% |
值得注意的是,第十五款 App 的 1 个错误是"重构引入"的,而不是 ArkTS 语法规则本身造成的。这意味着 ArkTS 的编译错误可以分成两类:规则性错误(不理解/不熟悉 ArkTS 语法)和重构错误(在修改已有代码时不小心引入的)。
规则性错误会随着经验积累而减少,重构错误则是每个开发者都会面临的挑战,无论使用什么语言。
10.4 十五款 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 | 绿植领养 | 重构也可能引入错误 |
11. ArkUI 开发经验再再总结
11.1 十五条铁律
经过十五款 App 的实践,新增一条关于重构的教训:
- Builder 不放逻辑 — 占编译错误 36%,最重要的规则
- 颜色声明接口 — 每次都忘,每次都错
- 数组修改用 concat — 不用展开运算符
- 弹窗用 if 包裹 — 不用 return
- ForEach key 独立作用域 — key 函数不能访问 index
- Row 不支持 borderBottomWidth — 用 Divider
- 检查残留代码 — 级联错误的根源
- 数据模型先行 — 先 interface 后 UI
- 紧凑风格 — Builder 越短错误越少
- 模式复用 — 新 App 用已验证模式
- setInterval 要清理 — aboutToDisappear 中清除
- @Builder 注解不能缺 — 第 13 款 App 的教训
- JSON.parse 需显式类型 — 用 Record<string, Object>
- Text 组件禁用变量声明 — Text 闭包只接受 Span
- 重构谨慎操作 — 每次修改都可能引入新错误
11.2 重构风险控制
第 15 条教训是本系列的最新发现。在绿植领养 App 中,重构 buildMyPage 时将两个独立的列表展示合并为一个 Builder 方法,这一重构引入了 1 个编译错误。虽然错误很小且修复迅速,但它揭示了一个重要原则:重构不是零成本的。
重构的风险控制建议:
- 小步提交:每次重构只改一个逻辑单元,重构完成后立即验证
- 先提取后删除:先创建新的 Builder/方法,验证可用后再删除旧代码
- 注意 let 的引入:将内联逻辑提取为方法时,注意不要在 Builder 中留下 let 声明
- Diff 审查:重构完成后,对照前后的代码差异,确认没有意外改动
11.3 从 22 到 1 的学习曲线
十五款 App 的错误数从第一款白噪音的 22 个下降到绿植领养的 1 个,下降了 95%。这个下降过程不是线性的,而是跳跃式的:
前 3 款:22 → 17 → 16(平均 18 个) — 探索期
第 4 款:1(断崖下降) — 模式复用的力量
第 5-8 款:12 → 12 → 10 → 4(持续下降) — 紧凑风格确立
第 9-12 款:11 → 11 → 3 → 8(波动) — 新类型引入
第 13-14 款:7 → 12(反弹) — 新 UI 模式尝试
第 15 款:1(再创新低) — 成熟期的稳定
数据揭示的真相:
- 模式复用是真有效的——第四款的断崖下降说明了这一点
- 新功能必然带来新错误——第 13 款的 @Builder 注解和第 14 款的 Text 组件限制
- 成熟期可以做到接近零错误——第 15 款的 1 个错误(且是重构引入的)
11.4 社交类 App 的开发模板
经过三款社交类 App 的实践,可以总结出一个通用的"C2C 社交 App 开发模板":
┌─────────────────────────────┐
│ 数据模型 │
│ ├── id (唯一标识) │
│ ├── owner (发起方) │
│ ├── receiver (接收方) │
│ ├── status (状态字段) │
│ └── metadata (业务字段) │
├─────────────────────────────┤
│ 三 Tab 架构 │
│ ├── Tab1: 浏览列表 (ForEach)│
│ ├── Tab2: 匹配/发现 (随机) │
│ └── Tab3: 我的 (双视角) │
├─────────────────────────────┤
│ 弹窗系统 │
│ ├── 创建弹窗 (表单) │
│ ├── 详情弹窗 (信息展示) │
│ └── 操作弹窗 (确认) │
├─────────────────────────────┤
│ 数据持久化 (Preferences) │
└─────────────────────────────┘
这个模板可以快速复用到类似的 C2C 应用中,如二手物品交换、技能共享、宠物寄养等。
12. 结语
12.1 十五款 App 的开发历程
App1 🎵 白噪音 → 初识 ArkUI
App2 ⏳ 时间胶囊 → 数据持久化
App3 🧊 冰箱剩菜 → Tab 架构
App4 😅 尴尬粉碎机 → 模式复用
App5 🛡️ 防骗训练 → 适老化
App6 💡 碎片学习 → 学习激励
App7 🐶 宠物日记 → 紧凑风格
App8 🗑️ 情绪垃圾桶 → 情感交互
App9 🧭 线下寻宝 → 社交互动
App10 🗡️ 订阅刺客 → 暗色主题
App11 🎑 声音明信片 → 模拟录音
App12 🎲 家庭大富翁 → 回合制游戏
App13 📚 二手书漂流瓶 → 随机匹配
App14 🧹 废话过滤器 → 自然语言检测
App15 🌱 绿植领养 → 缘分匹配
12.2 社交类 App 的设计思考
三款社交类 App(线下寻宝、图书漂流瓶、绿植领养)展现了社交应用在不同场景下的设计变化。
线下寻宝强调"位置"——藏宝和寻宝都依赖于地理坐标,社交发生在同一个物理空间。
图书漂流瓶强调"缘分"——随机匹配让每次"捞瓶子"都充满惊喜,社交是匿名的、单向的。
绿植领养强调"信息"——详细的植物信息(品种、分类、难度)帮助领养人做出决策,社交建立在信息透明度之上。
这三种不同的设计思路,对应着社交应用的三个核心要素:位置、随机性和信息。
12.3 ArkUI 的终极评价(第三次修订)
经过十五款 App 的实践,ArkUI 的评价体系已经非常完整。
优势:
- 声明式 DSL + @State 响应式机制成熟可靠
- 组件 API 持续改进(Grid、ForEach、Builder)
- 编译时类型检查有效(提前发现 95% 以上的问题)
- Preferences 数据持久化简单易用
不足:
- @Builder 语法约束过严(占编译错误 36%,仍是最大痛 Point)
- Text 组件构建器限制过多(Span 不能动态生成)
- 错误恢复能力不足(一个错误 = 多个级联错误)
- BorderOptions 不支持单边属性(与 Web CSS 差异大)
最新评价:
- 经过 15 款 App、约 10,200 行代码的验证,ArkUI 的稳定性是可靠的
- 编译错误的分布已经非常清晰,90% 以上的错误可以提前预防
- 对于掌握了"铁律"的开发者,ArkUI 可以是一个非常高效的开发框架
12.4 给开发者的建议(再追加)
- 先模式后代码:在开始一个新 App 之前,先确定它属于哪种模式(工具/社交/教育/游戏),然后套用对应的模板
- 重构警惕:即使经验丰富,每次重构也要保持警惕,一步一步验证
- 数据可视化:错误数趋势图是最直观的学习反馈——看着错误数从 22 降到 1,信心自然建立
- 社交 App 关注数据模型:C2C 应用的核心是数据如何在两个用户之间流转,先把数据模型设计好,UI 自然就清晰了
12.5 感谢与展望
十五款 App、十五篇博客、约 160,000 字——从 6 月 13 日到 6 月 14 日,完成了全部 App 开发和博客撰写。感谢每一位读者的陪伴。
错误数从 22 降到 1,代码行数从 1320 精简到 530——这些数字记录着学习的过程。但最重要的并不是数字本身,而是一路走来积累的模式、经验和信心。
现在,打开 DevEco Studio,去创造属于你自己的 App 吧。
附录 A:第十五款 App 核心代码
缘分匹配
pickSurprise(): void {
let available = this.list.filter(p => !p.isAdopted);
if (available.length === 0) return;
this.surpriseAnim = true;
this.surprisePlant = available[Math.floor(Math.random() * available.length)];
}
送养
doAdd(): void {
if (this.newName.trim() === '' || this.newGiver.trim() === '') return;
this.list = [{
id: Date.now(), name: this.newName.trim(),
species: this.newSpecies.trim() || '未知品种',
category: CATEGORIES[this.newCategory],
difficulty: DIFFICULTIES[this.newDifficulty],
location: this.newLocation.trim() || '未知地点',
giver: this.newGiver.trim(), date: Date.now(),
isAdopted: false, adopter: ''
}].concat(this.list);
this.showAdd = false; this.saveData();
}
领养
doAdopt(): void {
let p = this.selected as Plant;
p.isAdopted = true; p.adopter = this.adopterName.trim();
this.list = this.list.concat([]); this.saveData();
}
列表筛选
getFilteredList(): Plant[] {
if (this.filterAdopted) return this.list;
return this.list.filter(p => !p.isAdopted);
}
附录 B:系列速查
| 指标 | 数值 |
|---|---|
| App 数量 | 15 |
| 博客总字数 | ~160,000 字 |
| 代码总行数 | ~10,200 行 |
| 编译错误 | ~148 个 |
| @Builder 方法 | ~195 个 |
| 修复轮次 | 29 轮 |
更多推荐


所有评论(0)