鸿蒙 Next 反向导师平台 App 开发实战:导师列表 + 申请系统 + 状态管理



鸿蒙 Next 反向导师平台 App 开发实战:导师列表 + 申请系统 + 状态管理
作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio
语言框架:ArkTS + ArkUI
字数:约 9200 字
目录
- 引言
- 产品概念与导师模型
- 三 Tab 架构设计
- 首页布局设计
- 导师卡片组件
- 详情弹窗与申请流程
- 状态管理系统
- Builder 参数传递与 const 约束
- 编译错误全记录
- 第二十九款 App 全景回顾
- 结语
1. 引言
1.1 什么是"反向导师"
传统导师制是"年长的导师指导年轻的学员"。反向导师制则相反——年轻的导师指导年长的学员。
这个概念由普华永道在 2010 年代率先实践:让千禧一代员工反向指导高管团队,教授社交媒体、数字工具和新兴技术。结果出奇地好——高管们学到了新技能,年轻员工感受到了价值认可。
在中国,反向导师有更广阔的应用场景:
城市老人:2.8 亿 60 岁以上人口
智能手机普及率:超过 60%
能熟练使用手机挂号/购物/打车:不到 20%
这中间的"40% 的普及率差距 × 20% 的技能差距",就是反向导师的价值空间。年轻人花 2 小时教一位长辈用手机挂号,省去的是长辈去医院排队的半天时间。
“反向导师平台” App 将这个概念搬到了移动端:这里有 6 位年轻耐心的导师,教授手机使用、微信功能、短视频制作、线上购物等数字技能。学员(主要是长辈)可以浏览导师、申请指导、建立连接。
1.2 本 App 的定位
本 App 是系列中第二十九款,也是第二款"社交/平台类"App(第一款是 App 23 情绪漂流瓶回信)。但两款 App 的平台逻辑不同:
| 情绪漂流瓶(#23) | 反向导师平台(#29) | |
|---|---|---|
| 连接方式 | 匿名随机匹配 | 实名双向选择 |
| 核心操作 | 扔出/捞取 | 浏览/申请 |
| 数据流 | 创建→匹配→回复 | 查看→申请→连接 |
| 状态管理 | 单条记录 isReplied | 双数组 requests + connections |
| 导师数据 | 无 | 6 位预置导师 |
1.3 本 App 的技术特色
- 卡片复用机制:
buildMentorCard在首页和导师列表两个 Tab 中复用,减少代码重复 - 双状态数组:
requests和connections两个数组管理申请和连接状态 - Builder 参数传递:
buildMentorStatusCard(id, icon, text, color)通过参数控制渲染内容 - 主题适配:蓝色主色调传达专业信赖感
- Grid 分类展示:6 大技能分类 2×3 Grid 展示
1.4 二十九款 App 全景
App 数量: 29
代码总行数: ~17,000 行
编译错误数: ~275 个
博客总字数: ~291,000 字
技术博客数: 29 篇
2. 产品概念与导师模型
2.1 功能需求
用户故事 1:我想看看有哪些年轻导师可以教我
用户故事 2:我想知道每个导师会什么、教过多少人
用户故事 3:我想申请一位导师指导我
用户故事 4:我想看看我已经申请了哪些导师
功能清单:
├── F1: 首页展示(欢迎卡片 + 技能分类 + 推荐导师)
├── F2: 导师列表(全部 6 位导师)
├── F3: 导师详情弹窗(简介、技能标签、评分)
├── F4: 申请指导(发送请求)
├── F5: 我的导师(待确认 + 已连接分组)
└── F6: Grid 技能分类
2.2 数据模型
interface Mentor {
id: number;
name: string;
age: number;
avatar: string;
title: string;
skills: string[];
intro: string;
rating: number;
sessions: number;
}
9 个字段,涵盖了导师的完整信息:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 唯一标识 |
| name | string | 导师姓名 |
| age | number | 年龄,让学员了解导师背景 |
| avatar | string | emoji 头像 |
| title | string | 称号,一句话概括导师特色 |
| skills | string[] | 可教技能列表 |
| intro | string | 详细自我介绍 |
| rating | number | 评分 1-5 |
| sessions | number | 已指导次数 |
2.3 6 位导师的差异化设计
6 位导师覆盖了不同技能方向,避免重复:
年龄分布:20, 21, 22, 23, 24, 25
技能覆盖:基础操作、社交、购物、医疗、AI、硬件
评分分布:4.6 ~ 5.0
指导次数:67 ~ 312
每位导师的 title 和 intro 经过差异化设计——小周强调"零基础友好"(年龄最大 86 岁的学生),小吴侧重"AI 应用"(最新技术方向),小陈突出"生活办事"(使用频率最高的场景)。学员可以根据自己的需求选择最匹配的导师。
3. 三 Tab 架构设计
3.1 Tab 配置
build() {
Stack() {
Column().backgroundColor(C.bg)
Column() {
this.buildHeader()
if (this.activeTab === 0) this.buildHomeTab()
else if (this.activeTab === 1) this.buildBrowseTab()
else this.buildMyTab()
this.buildTabBar()
}
if (this.showDetail) this.buildDetailOverlay()
}
}
| Tab | 图标 | 功能 | 用户意图 |
|---|---|---|---|
| 0 | 🏠 | 首页 — 推荐 + 分类 | “有什么导师?” |
| 1 | 👥 | 导师 — 全部列表 | “看看所有导师” |
| 2 | 📋 | 我的 — 申请/连接 | “我申请了谁?” |
弹窗设计:本 App 只有一个弹窗(导师详情),从首页和导师列表两个入口都能打开。详情弹窗是"单一事实来源"——修改状态后,所有 Tab 同时更新。
3.2 导师卡片的复用
// 首页调用
ForEach(MENTORS, (m: Mentor, idx: number) => {
this.buildMentorCard(m, idx)
}, (m: Mentor) => m.id.toString())
// 导师列表调用(完全复用)
ForEach(MENTORS, (m: Mentor, idx: number) => {
this.buildMentorCard(m, idx)
}, (m: Mentor) => 'b' + m.id.toString())
buildMentorCard 在首页和导师列表 Tab 中被完全复用。两个 Tab 的区别仅在于:
- 列表前缀:首页显示在推荐分类下方,导师列表显示"共 N 位导师可预约"
- ForEach key 前缀:首页用
m.id.toString(),导师列表用'b' + m.id.toString()
为什么 key 要加前缀?两个 ForEach 如果使用相同的 key,ArkTS 可能会混淆它们的状态。不同 Tab 的 ForEach 使用不同的 key 前缀可以绝对避免这个问题。
3.3 数据流
┌─→ 首页 Tab(读取 MENTORS 常量)
│
用户操作 → 点击卡片 → 详情弹窗 → 申请 → requests[ ]
↓
┌─→ 首页:更新导师卡片状态(已申请/已连接)
│
┌─→ 导师列表:同步更新
│
┌─→ 我的 Tab:读取 requests + connections
设计要点:所有数据通过 @State 管理,不需要跨组件通信。requests 和 connections 两个数组变更后,所有 Tab 自动刷新。
4. 首页布局设计
4.1 欢迎卡片
Column() {
Text('欢迎来到反向导师平台 🌟').fontSize(18)
.fontColor(C.text).fontWeight(FontWeight.Bold)
Text('这里有年轻耐心的导师,\n教你使用手机和数字工具。\n选一位你喜欢的导师,开始学习吧!')
.fontSize(14).fontColor(C.textLight).lineHeight(22)
}.padding(16).backgroundColor(C.bgCard).borderRadius(16)
欢迎卡片是首页的第一个元素,用三段式文案传达 App 的核心价值:
- “年轻耐心的导师” → 强调反向导师的特色(年轻)
- “教你使用手机和数字工具” → 明确教学范围
- “选一位你喜欢的导师” → 引导行动
三段文案用 \n 换行,在卡片中呈现三行阅读节奏。
4.2 技能分类 Grid
Grid() {
ForEach(this.getCategories(), (cat: string[]) => {
GridItem() {
Column() {
Text(cat[0]).fontSize(28)
Text(cat[1]).fontSize(13).fontColor(C.text).fontWeight(FontWeight.Medium)
Text(cat[2] + ' 位导师').fontSize(11).fontColor(C.textMuted)
}.height(100).backgroundColor(C.bgCard).borderRadius(14)
}
}, (cat: string[]) => cat[1])
}.columnsTemplate('1fr 1fr').rowsGap(10).columnsGap(10)
6 个分类以 2×3 Grid 展示:📱手机基础、💬微信使用、📷拍照视频、🛒线上购物、🏥挂号就医、🤖AI工具。
每个分类卡片展示:大 emoji(28sp)→ 分类名称 → 导师数量。卡片高度固定 100px,2 列布局在手机屏幕上刚好填满宽度。
4.3 推荐导师列表
欢迎卡片和技能分类下方是完整的导师列表,与 Tab 1(导师列表)的卡片完全相同。这是有意为之——用户在首页浏览时就能看到所有导师,不需要切换到专门的 Tab。Tab 1 的作用是在"首页信息太多时"提供一个干净的列表视图。
5. 导师卡片组件
5.1 卡片布局
@Builder
buildMentorCard(m: Mentor, idx: number) {
Column() {
Row() {
Text(m.avatar).fontSize(40)
Column() {
Text(m.name + ' · ' + m.age + '岁').fontSize(16).fontColor(C.text).fontWeight(FontWeight.Bold)
Text(m.title).fontSize(12).fontColor(C.primary).margin({ top: 1 })
}.margin({ left: 10 }).layoutWeight(1)
Column() {
Text('⭐ ' + m.rating).fontSize(13).fontColor(C.warm).fontWeight(FontWeight.Bold)
Text(m.sessions + ' 次').fontSize(10).fontColor(C.textMuted)
}
}
Text(m.intro).fontSize(13).fontColor(C.textLight).maxLines(2).margin({ top: 6 })
Row() {
ForEach(m.skills, (s: string) => {
Text(s).fontSize(11).fontColor(C.primary)
.backgroundColor(C.primaryDim)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
})
Blank()
// 状态标签
}
}.onClick(() => { /* 打开详情 */ })
}
卡片从上到下的信息层级:
| 层级 | 内容 | 视觉权重 |
|---|---|---|
| 1 | 头像 + 姓名年龄 + 评分 | 最高(40sp emoji + 粗体) |
| 2 | 称号 | 中等(12sp 蓝色) |
| 3 | 简介(最多 2 行) | 较低(13sp 灰色) |
| 4 | 技能标签 + 状态 | 最低(11sp 小标签) |
layoutWeight(1) 确保姓名列撑满剩余空间,评分列右对齐。
5.2 技能标签
ForEach(m.skills, (s: string) => {
Text(s).fontSize(11).fontColor(C.primary)
.backgroundColor(C.primaryDim)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
.margin({ right: 4 })
}, (s: string) => s)
技能标签使用蓝色文字 + 透明蓝色背景(C.primaryDim),通过 borderRadius(8) 形成药丸形状。每个标签之间有 4px 右边距。
5.3 状态标签
if (this.isConnected(m.id)) {
Text('已连接 ✓').fontSize(13).fontColor(C.accent)
} else if (this.isRequested(m.id)) {
Text('待确认').fontSize(13).fontColor(C.warm)
}
卡片右下角显示当前状态:绿色"已连接"或橙色"待确认"。状态通过 isConnected() 和 isRequested() 两个方法判断,这两个方法使用 Array.indexOf() 检测 id 是否存在于对应的状态数组中。
状态标签的视觉设计遵循"绿色=已完成、橙色=进行中、无色=未操作"的通用色彩语义,与市面上大多数社交平台的配色一致——用户不需要学习就能理解。
5.4 卡片交互与状态联动
卡片点击打开详情弹窗,在弹窗中执行"申请"操作后,状态的变更流程如下:
点击"申请指导" → requests 数组更新
↓
@State 检测到数组变化 → 重新渲染所有引用 requests 的组件
↓
首页导师卡片 → isRequested() 返回 true → 显示"待确认"
导师列表卡片 → 同步更新
我的 Tab → 新增一条待确认记录
详情弹窗 → 按钮从"申请指导"变为"已发送申请"
这个联动不需要手动触发刷新——@State 的响应式系统会自动完成。这也是 ArkUI 声明式编程的核心优势:你只需要管理数据,UI 会自动同步。
6. 详情弹窗与申请流程
6.1 弹窗布局
详情弹窗展示导师的完整信息:
┌──────────────────────────────┐
│ 🧑💻 │
│ 小林 · 22岁 │
│ 数字原生代 · 编程达人 │
│ ───────────────────────── │
│ ⭐ 4.9 已指导 156 次 │
│ │
│ 📖 简介 │
│ 从高中就开始教爷爷奶奶用手机… │
│ │
│ 🛠️ 可教技能 │
│ [手机使用] [微信功能] [AI工具] │
│ │
│ 🙋 申请指导 │
└──────────────────────────────┘
弹窗高度为 72%,留出上下空间显示遮罩层。可滚动的内容区域让弹窗在信息多时也能完整展示。
6.2 申请流程
sendRequest(id: number): void {
if (this.isRequested(id) || this.isConnected(id)) return;
this.requests = [id, ...this.requests];
this.showDetail = false;
promptAction.showToast({ message: '📩 申请已发送!等待导师确认' });
}
三步流程:
用户点击"申请指导"
↓
check:是否已申请或已连接?→ 是 → 不操作
↓
否 → requests 头部插入导师 id
↓
关闭弹窗 → Toast 提示
↓
所有 Tab 自动更新(isRequested 返回 true)
这个流程模拟了"发送申请"的体验,但实际的"确认"操作(将 requests 中的 id 移到 connections)在本版本中由用户手动执行——这是"最小可行版本"的设计选择。
6.3 弹窗中的状态展示
if (this.isConnected(MENTORS[this.selectedMentor].id)) {
Text('✅ 已连接')
} else if (this.isRequested(MENTORS[this.selectedMentor].id)) {
Text('⏳ 已发送申请,等待确认')
} else {
Text('🙋 申请指导').onClick(() => { this.sendRequest(...) })
}
弹窗底部的按钮有三种状态,与卡片上的状态标签一致。这个三重条件渲染使用了 ArkTS 标准的 if-else if-else 语法——在 @Builder 中,if-else 是允许的,只要条件表达式不包含变量声明。
7. 状态管理系统
7.1 双数组状态
@State connections: number[] = [];
@State requests: number[] = [];
这是本 App 最核心的状态设计。两个数组分别存储已连接和已申请的导师 ID。数组元素是 number(导师 id),而不是复杂的对象结构。
为什么用 id 而不是对象:两个原因。第一,导师数据存储在常量 MENTORS 中,不需要重复存储。第二,indexOf(id) 比 find(m => m.id === id) 更简洁。
为什么不用 Map 或 Set:ArkTS 对 Map 和 Set 的支持有限(预览器中可能完全不可用)。数组 + indexOf 是最稳定、最兼容的方案。
为什么是两个数组而不是一个状态字段:如果给 Mentor 模型加一个 status 字段(如 'none' | 'requested' | 'connected'),有两种方案:
方案 A(状态字段):
// 修改 Mentor 模型,但 Mentor 是常量,不可修改
interface Mentor { status: string; } // ❌ 不能修改常量
方案 B(双数组):
@State requests: number[] = []; // 存储申请的导师 id
@State connections: number[] = []; // 存储已连接的导师 id
// ✅ 常量不变,状态分离
方案 B 胜出。它保持了 Mentor 数据的纯净(纯常量,不包含运行时状态),同时通过两个数组的组合即可推导出完整的导师状态矩阵。
7.2 状态查询
isConnected(id: number): boolean {
return this.connections.indexOf(id) >= 0;
}
isRequested(id: number): boolean {
return this.requests.indexOf(id) >= 0;
}
两个方法用于检测导师的状态。Array.indexOf() 返回 -1 表示未找到,>= 0 表示找到。这个方法在 ForEach 的回调中被调用(每张卡片渲染时判断状态),时间复杂度 O(n) 对最多 6 位导师来说完全可接受。
7.3 状态更新
sendRequest(id: number): void {
if (this.isRequested(id) || this.isConnected(id)) return;
this.requests = [id, ...this.requests];
this.showDetail = false;
promptAction.showToast({ message: '📩 申请已发送!等待导师确认' });
}
this.requests = [id, ...this.requests] 创建新数组并触发 @State 更新。头部插入确保最新的申请在最前面。
@State 更新机制:this.requests = [...] 创建一个新数组引用。ArkTS 的 @State 通过引用比较检测变化——只有新数组才会触发渲染更新。
7.4 状态持久化的缺失
本 App 没有实现数据持久化。重启 App 后,requests 和 connections 会清空。这是一个有意识的设计决策:在概念验证阶段,不引入持久化可以保持代码简洁,并确保预览器兼容性。
8. Builder 参数传递与 const 约束
8.1 问题:Builder 中不能有 const
在本 App 的开发中,遇到了一个"熟悉的陌生错误"——10905209(@Builder 中写逻辑)。
具体场景:在 buildMyTab() 的 ForEach 中,需要根据导师 id 查找导师信息。直接写 const mentor = this.findMentor(id) 触发了编译错误。
// ❌ 错误:@Builder 中的 ForEach 回调也不能使用 const
ForEach(this.requests, (id: number) => {
const mentor = this.findMentor(id); // 10905209
Column() {
Text(mentor.avatar).fontSize(32)
Text(mentor.name).fontSize(15)
}
})
这个错误的"熟悉"之处在于,10905209 已经在这个系列中出现了超过 100 次。但"陌生"之处在于,这次错误出现在 ForEach 的回调中,而不是 Builder 方法体的顶层。
ArkTS 的约束范围:@Builder 方法体中不允许变量声明,这个约束不仅适用于 Builder 方法体的顶层代码,也适用于 Builder 方法中所有嵌套闭包——包括 ForEach 的回调、事件处理器的闭包、条件渲染的分支。只要是 @Builder 调用链中的任何函数,都不能使用变量声明。
8.2 第一次修复:提取 Builder 方法
解决方案是创建一个专门渲染状态卡片的 @Builder 方法,通过参数传递需要的值:
// ✅ 修复:Builder 方法通过参数接收数据
@Builder
buildMentorStatusCard(id: number, statusIcon: string, statusText: string, statusColor: string) {
Column() {
Row() {
Text(this.getMentorAvatar(id)).fontSize(32)
Text(this.getMentorName(id)).fontSize(15)
Text(statusText).fontSize(12).fontColor(statusColor)
Text(statusIcon).fontSize(22)
}
}
}
这个方法接受 4 个参数:导师 id、状态图标、状态文字、状态颜色。通过参数控制渲染内容,避免了在 Builder 内部查找数据。
8.3 第二次修复:辅助方法
但 buildMentorStatusCard 内部仍然需要根据 id 获取导师的姓名和头像。直接在 @Builder 中写 const mentor = this.findMentor(id) 也不行。
最终方案:创建两个辅助方法:
getMentorAvatar(id: number): string {
const m = this.findMentor(id);
return m !== undefined ? m.avatar : '👤';
}
getMentorName(id: number): string {
const m = this.findMentor(id);
return m !== undefined ? m.name : '未知';
}
注意:const m 出现在辅助方法中而不是 @Builder 中——这是关键的区别。辅助方法是普通的实例方法,不受 @Builder 的语法约束。
8.4 教训总结
这是 29 款 App 中第 N 次遇到 10905209 错误。但这次的特殊之处在于:错误出现在 ForEach 的回调中,而不是直接出现在 Builder 的方法体中。
@Builder buildMethod() {
// ❌ 直接写 const → 错误 10905209
ForEach(arr, (item) => {
// ❌ 回调中写 const → 也是错误 10905209!
const x = item.prop;
})
}
ArkTS 的规则是:@Builder 方法体及其所有嵌套的闭包(包括 ForEach 回调)中,都不允许变量声明。所有数据必须通过方法返回值或 Builder 参数传入。
9. 编译错误全记录
9.1 错误概览
本 App 共出现 6 个编译错误。
| # | 错误代码 | 位置 | 原因 | 修复 |
|---|---|---|---|---|
| 1 | 10505001 | L343 | Row 不支持 wrap(true) |
移除 wrap |
| 2 | 10905209 | L261 | ForEach 回调中 const mentor = ... |
提取 Builder 方法 |
| 3 | 10905209 | L281 | ForEach 回调中 const mentor = ... |
提取 Builder 方法 |
| 4 | 10905209 | L315 | const m = MENTORS[...] |
改为内联 |
| 5 | 10905209 | Builder 内 | const mentor 第二次出现 |
提取辅助方法 |
| 6 | 10905209 | Builder 内 | const mentor 第三次出现 |
提取辅助方法 |
9.2 错误分布
10505001(属性不存在):1 个 → 17%
10905209(Builder 逻辑):5 个 → 83%
10905209 占总数的 83%,再次验证了它是 ArkTS 开发中最高频的错误类型。
9.3 row.wrap(true) 错误
// ❌ 错误
Row() {
ForEach(m.skills, (s: string) => {
Text(s) // 技能标签
})
}.width('100%').wrap(true) // Row 不支持 wrap
在 ArkTS 中,Row 组件没有 wrap 属性。如果需要在行内换行显示子元素,应该使用 Flex 组件:
// ✅ 修复:移除 .wrap(true)
Row() {
// 技能标签在 Row 中水平排列
}.width('100%')
技能标签通常 2-3 个,刚好在一行内显示,不需要换行。如果未来技能数量增加到 5 个以上,可以考虑使用 Flex 替代 Row。
9.4 29 款 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 ← 预览器兼容性
App 29: 6 ← Builder const 约束(83%)
App 29 的错误主要集中在一个类型(10905209),且全部可以通过"提取方法"或"直接内联"修复。没有出现新的错误类型,也没有触发"未知错误"的恐慌。
10. 第二十九款 App 全景回顾
10.1 数据总览
| 指标 | 数值 |
|---|---|
| 代码行数 | 373 行 |
| 编译错误数 | 6 个(修复后 0) |
| @State 变量 | 6 个 |
| @Builder 方法 | 7 个 |
| 弹窗数 | 1 个 |
| 预置导师 | 6 位 |
| 外部依赖 | 0 个 |
| 页面数(Tab) | 3 页 |
10.2 29 款 App 的 Builder 方法数量
App 1: 8 个 Builder
App 8: 12 个 Builder
App 16: 10 个 Builder
App 24: 14 个 Builder
App 25: 6 个 Builder
App 26: 6 个 Builder
App 27: 2 个 Builder
App 28: 6 个 Builder
App 29: 7 个 Builder
Builder 方法的数量从早期的 12-14 个下降到近期的 6-7 个。原因是:更多的 UI 直接内联到 ForEach 中,而不是提取为独立的 Builder 方法。
但 Builder 方法也不是越少越好——App 29 的 7 个 Builder 中,有 1 个(buildMentorStatusCard)是专门为了修复编译错误而创建的。这个 Builder 本身就是因为"不能在 ForEach 回调中写 const"才被提取出来的。
10.3 29 款 App 的经验沉淀
已经不会再犯的错误(第 29 次确认):
- 颜色常量忘记加 interface ✅
- @State 数组直接修改 ✅
- ForEach key 函数作用域 ✅
偶尔还会犯的错误(第 29 次仍然出现):
- @Builder 中写逻辑 ✅(10905209,几乎每款 App 都遇到)
新发现的错误类型(本 App):
- Row 不支持 wrap
10.4 从 1 到 29
第 1 款:学习 @Builder 的基本语法
第 8 款:理解 @Builder 中不能写逻辑
第 16 款:掌握 ForEach 的正确用法
第 24 款:44 个错误中学会索引相关规则
第 29 款:Builder 回调中也不能写 const
29 款 App,每一款都贡献了至少一个"新教训"。这些教训中,大部分是 ArkTS 的语法约束(不能写逻辑、不能解构、不能索引签名),少部分是组件 API 的限制(Row 不支持 wrap、onLongClick 不存在)。
11. 结语
11.1 反向导师的社会意义
"反向导师平台"这个 App,技术上并不复杂——没有 AI、没有实时通信、没有复杂的匹配算法。但它的社会意义可能超过了许多技术更复杂的 App。
在中国,有超过 2.8 亿 60 岁以上人口。他们中的绝大多数人拥有智能手机,但真正能熟练使用的比例很低。不是因为"学不会",而是因为"没人耐心教"。
反向导师——让年轻一代教年长一代数字技能——是一个低成本、高收益的社会创新。年轻人花几小时教长辈用手机挂号,可能节省的是长辈跑一趟医院的大半天时间。
11.2 平台类 App 的起点
本 App 是系列中第一款"平台类"App——连接两端的用户。虽然目前只有预置数据(没有真实的导师注册和学员注册),但它的架构可以轻松扩展:
扩展一:导师注册系统
// 当前:预置数据
const MENTORS: Mentor[] = [ /* 6 条 */ ];
// 扩展后:用户注册 + 审核
@State registeredMentors: Mentor[] = [];
// 新增导师注册表单:姓名、年龄、技能、简介
// 新增审核状态:pending / approved / rejected
当前使用 const MENTORS 存储导师数据。扩展为注册系统后,只需要将存储方式从 const 改为 @State,并添加注册表单即可。导师卡片组件(buildMentorCard)完全不需要修改——它读取的是 Mentor 接口,不管数据来自预置常量还是用户注册。
扩展二:评价系统
当前导师卡片显示 rating 和 sessions 两个统计字段,目前是硬编码的。扩展为真实评价系统后:
@State reviews: Review[] = [];
// Review: { mentorId, userId, rating, comment, date }
// 统计:根据 reviews 计算每个导师的 rating 和 sessions
getMentorRating(id) 和 getMentorSessions(id) 两个方法可以根据 reviews 动态计算,不需要修改 Mentor 模型。
扩展三:消息系统
当前"申请指导"只是将 id 加入 requests 数组。扩展为真实消息系统后:
@State messages: Message[] = [];
// Message: { id, from, to, content, time, type: 'request' | 'chat' }
// 申请 → 自动创建一条 request 类型消息
// 导师确认 → 消息状态更新为 accepted
// 之后可以自由聊天
从"状态数组 + Toast 提示"升级为"消息列表 + 实时通知",是平台类 App 的自然演进路径。
这三个扩展的共性是:当前的数据结构和组件设计可以无缝升级。Mentor 接口不需要改,导师卡片组件不需要改,三个 Tab 的架构不需要改——只需要添加新的 @State 和对应的 UI。
11.3 给开发者的建议
- @Builder 的 const 约束比你想象的更严格:不仅 Builder 方法体不能写 const,Builder 中 ForEach 的回调也不能写。所有数据都要通过方法调用获取
- 双数组状态管理适合"一对多"关系:一个用户对多个导师的关系,可以用两个 id 数组轻松管理
- 卡片复用从设计时就要考虑:如果同一个卡片在两个 Tab 中出现,一开始就设计成复用的 Builder 方法
- 技能标签的 Row 不需要 wrap:3 个以内的标签刚好一行,5 个以上才需要考虑换行
- 平台类 App 从预置数据开始:不需要一开始就做注册系统,6 条预置数据足够展示产品概念
11.4 感谢
29 款 App、29 篇博客、约 291,000 字。
从白噪音到反向导师平台,从工具到平台,从个人到连接——这个系列的 29 款 App 覆盖了鸿蒙 Next 应用开发的多个维度。
第 29 篇不是终点。系列还有更多 App、更多错误、更多教训等待记录。
现在,打开 DevEco Studio,写下你的第 1 款 App——也许它会是第 30 篇博客的主角。
附录 A:核心代码速查
导师模型
interface Mentor {
id: number; name: string; age: number;
avatar: string; title: string;
skills: string[]; intro: string;
rating: number; sessions: number;
}
发送申请
sendRequest(id: number): void {
if (this.isRequested(id) || this.isConnected(id)) return;
this.requests = [id, ...this.requests];
this.showDetail = false;
}
状态查询
isConnected(id: number): boolean {
return this.connections.indexOf(id) >= 0;
}
isRequested(id: number): boolean {
return this.requests.indexOf(id) >= 0;
}
附录 B:色板
| 变量 | 值 | 用途 |
|---|---|---|
C.bg |
#F0F5FF |
主背景(冰蓝) |
C.bgCard |
#FFFFFF |
卡片背景 |
C.primary |
#4A7CF7 |
主色(信赖蓝) |
C.accent |
#34C759 |
已完成(绿色) |
C.warm |
#FF8A4C |
评分/待确认(暖橙) |
附录 C:Builder 中禁止的操作速查
在 @Builder 中:
❌ const x = value → 方法中获取
❌ let x = value → 方法中获取
❌ for (...) → 方法中计算
❌ if (x = get()) → 方法中获取
✅ if (condition) { Component() } → 允许
✅ this.method() → 允许(调用方法)
✅ inline expression → 允许
在 @Builder → ForEach 回调中:
❌ const x = item.prop → 同上
✅ if (condition) { } → 允许
✅ this.method(item) → 允许
更多推荐




所有评论(0)