ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于ArkTS的鸿蒙家教APP开发:状态管理与列表渲染工程实践

基于ArkTS的鸿蒙家教APP开发:状态管理与列表渲染工程实践 简介在鸿蒙生态加速发展的背景下这套基于ArkTS框架开发的倾心家教APP设计源码面向希望掌握鸿蒙应用开发、熟悉TypeScript/JavaScript工程实践的开发者提供了一个可直接研读的完整参考工程。它围绕家教服务场景覆盖课程管理、教师资源调度、学习进度跟踪等常见模块适合作为课程设计或项目二次开发的起点。压缩包共97个文件、867KB包含43个PNG图片、22个ArkTSets页面文件、9个JSON5与9个JSON配置文件、4个TypeScript源文件以及hvigor构建脚本、license、gitignore等工程辅助文件ets文件对应ArkUI界面实现JSON/JSON5承担模块与构建配置图片用于UI图标和视觉素材。目前已有235人学习下载。借助这份源码读者可以系统观察鸿蒙工程目录结构理解ArkTS声明式UI写法、配置项组织与hvigor打包流程获得一个家教APP从界面到业务的落地范例。1. 用 ArkTS 写鸿蒙家教 APP为什么先把数据模型想清楚一个家教 APP 的界面看起来是列表、筛选、预约、订单但真正决定开发效率的是数据在页面之间怎么流动。用 ArkTS 开发鸿蒙应用时这个问题尤其明显ArkTS 是声明式 UI 范式界面会随状态自动刷新但如果状态设计得混乱一个老师的排期变了可能触发整个列表重绘甚至导致组件状态丢失。这篇内容聚焦的是基于 ArkTS 框架的鸿蒙倾心家教 APP 设计源码背后那套常见做法——从状态管理、网络层、列表渲染到调试排错的完整链路适合正在用 DevEco Studio 做鸿蒙应用开发、或者准备把原有家教/预约类业务迁移到鸿蒙生态的工程师。鸿蒙的 ArkTS 语法风格接近 TypeScript但装饰器、状态管理和组件通信都是独立体系网上免费源码大全里的鸿蒙项目大多还停留在 Java 或 JS 版本真正用 ArkTS 写业务逻辑的参考并不多下面按一套可落地的方案拆开讲。2. ArkTS 声明式状态管理与组件通信Page 到组件树的完整链路2.1 声明式 UI 的最小结构Entry 与 Component 的职责边界每个 ArkTS 页面由一个主组件承载使用Entry标记页面内部再拆成若干子组件。看源码时先找Entry修饰的 struct那是页面的入口。一个典型的家教 APP 首页结构如下Entry Component struct HomePage { State teacherList: TeacherInfo[] []; build() { Column() { TeacherListHeader() TeacherList({ teachers: this.teacherList }) } .onAppear(() { this.loadTeachers(); }) } async loadTeachers() { // 请求数据后更新 teacherListUI 会自动刷新 } }Entry标识页面根组件Component代表可复用组件单元。子组件TeacherList通过构造参数接收父组件传值父组件持有数据源。这种模式下数据是单向的父组件修改状态子组件只负责展示。需要理解的是Entry并不限制一个页面只能有一个组件它只负责标记入口。子组件里如果涉及业务逻辑也应该在自己的build()内完成布局组合而不是把逻辑全部堆到页面级。看别人源码时先看 struct 的装饰器再看构造参数列表基本能判断这个组件的职责边界。2.2 State 的可见范围与 Prop/Link 的传递方向新手最容易出问题的是状态装饰器的选择。State 标记的变量只能在当前组件内部修改子组件拿到的只是初始值快照子组件如果要修改父组件的状态得用 Link 进行双向绑定或者通过回调函数通知父组件。Component struct FilterBar { Prop subject: string; // 单向父组件传给子组件展示 Link selectedIndex: number; // 双向子组件可以修改父组件的状态 build() { Row({ space: 8 }) { Text(数学) .onClick(() { this.selectedIndex 0; // 直接改父组件变量 }) } } }上面的代码体现了两种典型传递方式Prop适合筛选条件展示Link适合子组件选择科目、父组件列表联动刷新这种交互。在鸿蒙面试题里这两个装饰器是高频考点实际开发中选错会导致数据不同步或无限刷新循环。关于 Link 有一个很容易忽略的细节它要求父组件传入的必须是 State或 Link修饰的变量不能直接传一个字面量。如果父组件里写selectedIndex: 2编译不会报错但子组件修改时毫无效果排查起来隐蔽。2.3 用 Builder 复用家教卡片组件ArkTS 里的 Builder 是一种轻量级的 UI 复用方案适合在一个页面内重复使用的局部片段。比如家教列表里的老师卡片不同列表页推荐、收藏、历史展示样式一致只有点击行为不同就可以用 Builder 提取Builder TeacherCard(teacher: TeacherInfo, onAction: () void) { Column() { Row() { Text(teacher.name).fontSize(16).fontWeight(FontWeight.Bold) Blank() Text(${teacher.pricePerHour}元/时).fontColor(#FF6B35) } Text(teacher.subject).fontSize(13).fontColor(#888888) } .padding(12) .backgroundColor(#FFFFFF) .borderRadius(8) .onClick(onAction) }Builder 函数与普通函数最大的不同在于它内部可以声明 UI 结构并且接收参数决定渲染内容。适合三种场景同款卡片在不同页面复用、同一个卡片要支持动态 Slot 内容、列表项的 UI 组装。源码里看到this.TeacherCard(...)这样的调用方式都是在复用其 UI 逻辑。装饰器方向典型场景注意事项State组件内部页面数据源不能跨组件直接传递Prop父→子展示类配置项值变化会触发子组件刷新Link双向子组件修改父状态父组件必须传入 State 变量Builder复用卡片、列表项 UI内部可用条件判断和循环表里这四种机制是 ArkTS 状态体系的基础。实际写代码时建议先画一个哪个组件拥有数据的图再决定装饰器选型不要边写边改。3. 鸿蒙家教 APP 的数据层接口定义、http 请求与参数配置3.1 用 interface 约束服务端返回结构家教 APP 的数据层主要围绕老师列表、老师详情、预约请求、订单列表四类接口。ArkTS 里使用interface定义数据类型比直接用任意对象安全得多export interface TeacherInfo { id: string; name: string; subject: string; pricePerHour: number; rating: number; introduction: string; availableSlots: TimeSlot[]; } export interface TimeSlot { date: string; startTime: string; endTime: string; booked: boolean; } export interface TeacherListResponse { code: number; message: string; data: { total: number; list: TeacherInfo[]; }; }这里有两个设计要点。一是嵌套结构availableSlots单独定义类型方便后续在可预约时间组件里复用二是响应体包裹了 code/message/data网络层需要统一解包而不是在业务代码里重复判断。这种写法配合 later 的 JSON 转对象可以让编译期提前拦截字段拼写错误。3.2 封装统一请求方法不封装直接写http.createHttp()也可以但多个页面重复写 header、超时、错误处理代码会失控。一个常见做法是抽一个请求基类import http from ohos.net.http; export class ApiClient { private static readonly BASE_URL https://api.example.com/v1; private static readonly TIMEOUT 10000; static async getT(path: string, params?: Recordstring, string): PromiseT { const httpRequest http.createHttp(); const url params ? ${this.BASE_URL}${path}?${this.stringify(params)} : ${this.BASE_URL}${path}; const response await httpRequest.request(url, { method: http.RequestMethod.GET, header: { Content-Type: application/json }, connectTimeout: this.TIMEOUT, readTimeout: this.TIMEOUT, }); if (response.responseCode 200) { const result JSON.parse(response.result as string) as ApiResponseT; if (result.code 0) { return result.data; } else { throw new Error(业务错误: ${result.message}); } } else { throw new Error(HTTP 错误: ${response.responseCode}); } } private static stringify(params: Recordstring, string): string { return Object.keys(params).map(key ${key}${encodeURIComponent(params[key])}).join(); } }代码说明使用http.createHttp()创建连接request()方法发起请求connectTimeout控制连接阶段超时readTimeout控制响应阶段超时两个参数分开设置弱网环境可以单独调大 readTimeout返回结果通过responseCode判断 HTTP 状态再通过业务 code 判断逻辑是否成功。使用时的调用方式const teachers await ApiClient.getTeacherListResponse(/teacher/list, { subject: this.selectedSubject, page: 1, pageSize: 20 });3.3 http 模块常用参数与坑参数默认值建议值说明connectTimeout600010000连接超时弱网下太短容易失败readTimeout600015000响应超时家教列表接口若查库较慢需要放宽usingCachetruefalse改成 false避免列表数据被缓存后不刷新expectDataTypestringstring默认即可自己 JSON.parse 更好控制headerContent-Type 默认缺省显式声明部分后端要求 charsetutf-8提示在 DevEco Studio 里联调时模拟器访问本机服务需要用 10.0.2.2 代替 localhost真机调试则必须保证手机和电脑在同一网段。4. 家教老师列表、筛选与预约下单的落地实现4.1 筛选参数的设计扁平化优于嵌套家教 APP 的筛选条件通常包括科目、年级、价格区间、教学方式线上/线下。接口参数的设计上建议全部使用扁平字段不用嵌套 JSONinterface TeacherQuery { subject?: string; grade?: string; minPrice?: number; maxPrice?: number; mode?: online | offline; page: number; pageSize: number; }扁平参数的好处有两个一是拼接 URL 简单直接遍历键值对二是服务端做查询优化时可以直接映射查询条件不用递归解析。筛选 UI 一般做成横向滚动的标签栏点击标签时更新查询条件并重置 page 为 1State query: TeacherQuery { page: 1, pageSize: 10 }; selectSubject(subject: string) { this.query.subject subject; this.query.page 1; this.loadTeachers(); }重置 page 是筛选功能最容易漏掉的一点。如果保持在当前页筛选后列表可能只剩几页数据但你的分页逻辑还在按旧页码请求产生明明筛了数学却看到语文老师的假象。4.2 分页加载与下拉刷新的实现列表用List组件承载配合onReachEnd触发下一页加载onAppear触发首次加载State teacherList: TeacherInfo[] []; State hasMore: boolean true; private currentPage: number 1; private readonly pageSize: number 20; build() { List({ space: 12 }) { ForEach(this.teacherList, (teacher: TeacherInfo) { ListItem() { this.TeacherCard(teacher, () this.goToDetail(teacher.id)) } }, (teacher: TeacherInfo) teacher.id) } .onReachEnd(() { if (this.hasMore) { this.currentPage; this.loadTeachers(); } }) } private async loadTeachers() { try { const res await ApiClient.getany(/teacher/list, { page: this.currentPage.toString(), pageSize: this.pageSize.toString() }); if (res.data.list.length 0) { this.teacherList [...this.teacherList, ...res.data.list]; } else { this.hasMore false; } } catch (e) { this.currentPage--; // 请求失败回退页码 } }关键在于onReachEnd触发加载时先增加页码请求失败要回退hasMore控制是否继续加载ForEach 的第三个参数是键生成函数使用教师 id 避免列表项位置错乱导致组件复用异常。4.3 预约下单的状态流转预约是家教 APP 的核心动作从选中老师到确认下单要经历选时间 → 填写信息 → 提交订单 → 等待老师确认四个状态。前两个状态可以在页面内用普通变量控制第三个状态涉及网络请求必须加 loadingState submitting: boolean false; async submitOrder() { if (this.submitting) return; this.submitting true; try { await ApiClient.post(/order/create, { teacherId: this.selectedTeacher.id, slotId: this.selectedSlot.id, remark: this.remark }); this.showToast(预约成功等待老师确认); } finally { this.submitting false; } }submitting标志位是为了防止连点导致重复提交。后端即使做了幂等前端也不该把压力全丢给接口。这里的业务状态不建议用全局状态管理工具因为这已经是排他性的操作页面内管理足够。5. 多选列表删除、动态渲染与调试状态同步的三个坑5.1 直接 splice() 为什么列表不刷新鸿蒙里用 State 修饰数组修改元素时有两个选择this.teacherList.splice(index, 1)或this.teacherList this.teacherList.filter(...)。前者是数组变异方法State 的观测机制可能捕获不到索引级变化导致 UI 不更新。// 可能不生效直接变更数组内容State 未触发通知 this.teacherList.splice(index, 1); // 推荐生成新数组State 能检测到引用变化 this.teacherList this.teacherList.filter(item item.id ! targetId);这是一个典型的 ArkTS 与 JS 原生的差异点开发鸿蒙应用时不改掉这个习惯会踩很多坑多选删除场景几乎必现。类似的还有直接修改数组某个对象的属性State 只观测到数组引用不变属性变化不会触发刷新。建议统一用创建新对象/新数组的方式。5.2 ForEach 与 LazyForEach 的性能取舍列表数据量小于 20 条时ForEach 完全够用。家教 APP 的老师列表如果做了条件筛选通常返回量不大但全部老师模式可能上百条这时每项包含图片、文字、标签ForEach 会一次性创建所有子组件页面容易出现卡顿。LazyForEach 按需创建组件只渲染可视区附近的项private teacherData: TeacherDataSource new TeacherDataSource(); build() { List() { LazyForEach(this.teacherData, (teacher: TeacherInfo) { ListItem() { this.TeacherCard(teacher, () {}) } }, (teacher: TeacherInfo) teacher.id) } }使用 LazyForEach 的前提是实现IDataSource接口的totalCount()和getData(index)方法。注意LazyForEach 必须配稳定的键生成函数否则滚动时组件会错位刷掉的内容可能又冒出来。这个组装过程网上源码包里有大量现成写法拿过来用之前先看它是否实现了必须的接口方法。5.3 输出调试console 日志在鸿蒙里的正确姿势ArkTS 调试时用console.info会输出到 DevEco Studio 的 Log 窗口但这套日志体系是有级别的filter 默认可能不会显示 verbose 级信息。常用两种方式console.info([TeacherList] 加载老师列表: ${this.teacherList.length} 条); // 网络请求前后加日志方便定位是参数错误还是返回异常 console.info([Request] GET /teacher/list page${page}); try { const res await ApiClient.get(...); console.info([Response] code${res.code}); } catch (e) { console.error([Error] ${JSON.stringify(e)}); }鸿蒙的 console 不是浏览器环境的 console它没有像 web 端console.table那样的格式化输出对象需要JSON.stringify才能看清结构。另一个实用技巧是DevEco Studio 的 AppInspector 可以查看组件树如果 UI 显示异常先在 Inspector 里确认组件是否存在、状态值是什么再决定是改逻辑还是改布局。6. 上线前的参数清单从超时配置到弱网验证6.1 参数映射表根据前面几章的代码实现整理一份可直接抄的配置清单配置项参数建议值修改位置连接超时connectTimeout10000msApiClient 类响应超时readTimeout15000msApiClient 类分页大小pageSize20页面查询条件预加载触发距离cachedCount3LazyForEach 构造参数下拉刷新阈值offset80vpRefresh 组件列表项图片缓存setImageCache默认开启Image 组件属性失败重试次数retry1自行封装重试逻辑请求缓存usingCachefalsehttp 模块配置这些参数在新机型和旧机型上的表现差异不小numeric 设备建议用真机测试时重新调整。6.2 验证流程三个必须做第一弱网模拟。DevEco Studio 自带的 Network 模拟工具可以设置丢包率和延迟。把延迟加到 2000ms此时检查下拉刷新时是否重复 loading、onReachEnd在请求未返回时是否被再次触发。缺少loading标志位的话快速滚动列表会发出十几个重复请求。第二多设备验证。ArkTS 的State在 API 9 及以上版本有StorageLink等增强能力但旧版本设备上可能出现状态同步异常。至少拿一台 API 10 和一台 API 12 的设备分别跑一遍筛选老师 - 预约下单 - 取消预约这条主链路。第三内存检查。用 DevEco Studio 的 Profiler 录制滑动列表的内存曲线如果列表页离开后内存不回收检查是否在aboutToDisappear里取消了未完成的网络请求。高频出现的卡顿往往不是列表渲染造成的而是图片组件没有被正确释放。最后补充一个冷门做法给列表页组件加上Reusable装饰器让列表项在滚动时被复用这是鸿蒙里针对长列表组件复用开销的常用优化手段对家教 APP 这种卡片密集型的页面效果最直接。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进