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

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

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


目录

  1. 引言
  2. 产品概念与数据模型
  3. 三 Tab 架构设计
  4. 花圃列表与筛选机制
  5. 缘分匹配引擎
  6. 送养与领养流程
  7. 养护难度分级系统
  8. 双视角数据展示
  9. 编译错误全记录
  10. 十五款 App 全景回顾
  11. ArkUI 开发经验再再总结
  12. 结语

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(养护难度)。前者让绿植信息更具体,后者帮助领养人判断是否适合自己。双视角设计的核心仍然是 giveradopter 两个字段——通过昵称匹配实现"我送养的"和"我领养的"两个视图。

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 也有类似的随机匹配功能(“捞瓶子”),但两者有两个关键区别:

  1. 匹配后操作:图书漂流瓶匹配后只能"查看详情"或"换一本",领养操作在详情页完成。绿植领养在匹配卡片上直接提供了"我要领养"按钮,缩短了操作路径。

  2. 空状态位置:图书漂流瓶的空状态在卡片位置,绿植领养的空状态占据了整个 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 的实践,新增一条关于重构的教训:

  1. Builder 不放逻辑 — 占编译错误 36%,最重要的规则
  2. 颜色声明接口 — 每次都忘,每次都错
  3. 数组修改用 concat — 不用展开运算符
  4. 弹窗用 if 包裹 — 不用 return
  5. ForEach key 独立作用域 — key 函数不能访问 index
  6. Row 不支持 borderBottomWidth — 用 Divider
  7. 检查残留代码 — 级联错误的根源
  8. 数据模型先行 — 先 interface 后 UI
  9. 紧凑风格 — Builder 越短错误越少
  10. 模式复用 — 新 App 用已验证模式
  11. setInterval 要清理 — aboutToDisappear 中清除
  12. @Builder 注解不能缺 — 第 13 款 App 的教训
  13. JSON.parse 需显式类型 — 用 Record<string, Object>
  14. Text 组件禁用变量声明 — Text 闭包只接受 Span
  15. 重构谨慎操作 — 每次修改都可能引入新错误

11.2 重构风险控制

第 15 条教训是本系列的最新发现。在绿植领养 App 中,重构 buildMyPage 时将两个独立的列表展示合并为一个 Builder 方法,这一重构引入了 1 个编译错误。虽然错误很小且修复迅速,但它揭示了一个重要原则:重构不是零成本的

重构的风险控制建议:

  1. 小步提交:每次重构只改一个逻辑单元,重构完成后立即验证
  2. 先提取后删除:先创建新的 Builder/方法,验证可用后再删除旧代码
  3. 注意 let 的引入:将内联逻辑提取为方法时,注意不要在 Builder 中留下 let 声明
  4. 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(再创新低)           — 成熟期的稳定

数据揭示的真相

  1. 模式复用是真有效的——第四款的断崖下降说明了这一点
  2. 新功能必然带来新错误——第 13 款的 @Builder 注解和第 14 款的 Text 组件限制
  3. 成熟期可以做到接近零错误——第 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 给开发者的建议(再追加)

  1. 先模式后代码:在开始一个新 App 之前,先确定它属于哪种模式(工具/社交/教育/游戏),然后套用对应的模板
  2. 重构警惕:即使经验丰富,每次重构也要保持警惕,一步一步验证
  3. 数据可视化:错误数趋势图是最直观的学习反馈——看着错误数从 22 降到 1,信心自然建立
  4. 社交 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 轮

Logo

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

更多推荐