AI书单推荐 —— HarmonyOS 原生AI应用开发实战技术博客
AI书单推荐 —— HarmonyOS 原生AI应用开发实战技术博客
摘要:本文以"AI书单推荐"应用的完整开发过程为例,深入剖析在 HarmonyOS 平台上使用 ArkTS/ArkUI 进行原生 AI 应用开发的全流程。内容涵盖从需求对齐、架构设计、任务分解、质量审核到自动化实现和复盘评估的六个阶段,展示了如何利用 HarmonyOS 的声明式 UI 框架、@State 状态驱动机制和 MVVM 架构模式,构建一个兼具美观交互与智能推荐能力的端侧应用。本文适合 HarmonyOS 应用开发者、ArkTS 初学者以及对人机交互与 AI 应用结合感兴趣的开发者阅读。

1. 对齐阶段(Align)
1.1 项目上下文分析
技术栈全景
"AI书单推荐"应用运行在 HarmonyOS 生态之上,其核心技术栈如下:
| 技术维度 | 选型方案 | 版本/规格 |
|---|---|---|
| 操作系统 | HarmonyOS | API 6.0.1 (21) |
| 开发语言 | ArkTS(基于TypeScript的静态类型方言) | Stage Mode |
| UI框架 | ArkUI(声明式UI框架) | 组件化+@State驱动 |
| 开发工具 | DevEco Studio | 最新版 |
| 打包构建 | Hvigor 构建系统 | oh-package.json5 |
| 路由方案 | router 模块(@kit.ArkUI) | 页面级跳转 |
| 数据模型 | 纯 ArkTS 类定义 | 无第三方依赖 |
架构模式分析
整个项目采用 MVVM(Model-View-ViewModel) 架构范式,在 ArkTS 的约束下做了适应性调整:
- Model 层:
AI书单推荐Data类,承载所有数据结构和业务实体。在 ArkTS 中,类声明引入的是一种新类型,而非值,因此天然适合作为数据模型。 - View 层:
AI书单推荐Page结构体,使用@Component装饰器标记为 UI 组件,通过@State装饰器声明响应式状态变量,驱动 UI 自动刷新。 - Service 层:
AI书单推荐Service类,封装核心业务逻辑,包括数据生成和处理,属于 ViewModel 的变体实现。
依赖关系分析
从项目根目录的 build-profile.json5 可以看到,应用的目标 SDK 版本为 6.0.1(21),采用 stageMode 阶段化运行模式。oh-package.json5 中无第三方依赖,体现了 HarmonyOS 原生开发"零外部依赖"的指导思想——所有能力均来自系统 Kit。
entry/src/main/ets/apps/AI书单推荐/
├── AI书单推荐Page.ets # 视图层(View)
├── AI书单推荐Model.ets # 数据模型层(Model)
└── AI书单推荐Service.ets # 业务逻辑层(Service/ViewModel)
1.2 需求理解确认
原始需求
"AI书单推荐"的核心需求是:用户输入个人阅读兴趣、阅读水平和目标,应用基于这些输入智能推荐适合的书籍清单,并给出每本书的详细信息(作者、分类、难度、评分、推荐理由、关键收获、阅读顺序、搭配建议等)。
边界确认
| 维度 | 边界定义 |
|---|---|
| 输入范围 | 兴趣、阅读水平、目标三个文本字段 |
| 输出范围 | 书籍列表 + 15个维度的书籍详细信息 |
| 设备范围 | 仅支持手机(phone)设备类型 |
| 交互方式 | 文本输入 + 按钮触发 + 结果展示 |
| 网络依赖 | 当前版本无网络依赖(离线Mock数据) |
| 数据持久化 | 无需持久化,结果仅当次展示 |
需求理解深化
在深入阅读源代码后,我梳理出以下关键需求点:
- 输入采集:用户需要提供三个维度的信息——兴趣(如"科幻小说")、阅读水平(如"中级")、目标(如"拓展视野")。这些信息通过
TextInput组件采集,以键值对形式存储在inputData中。 - 智能推荐:点击"整理书架"按钮后,Service 层根据输入生成推荐结果。当前版本使用 Mock 数据作为演示,预留了接入真实 AI 模型的接口。
- 结果展示:推荐结果以卡片形式展示在页面下方,包含书籍列表和多维度详情信息,用户可直观查看推荐内容。
- 导航交互:页面顶部提供返回按钮,支持从主应用列表页(Index.ets)跳入后返回。
1.3 疑问澄清与决策
在开发过程中,遇到了多个关键决策点,每个决策都需要在 ArkTS 语法约束和实际开发需求之间找到平衡。
决策1:ArkTS 语法约束下的状态管理
疑问:ArkTS 不支持 any 和 unknown 类型,也不支持索引签名,如何管理动态表单数据?
上下文分析:在书单推荐应用中,用户输入包含三个字段——兴趣、阅读水平和目标。这些字段是动态的,未来可能增加或减少。在传统 TypeScript 中,我们可能会使用 any 或 {[key: string]: string} 索引签名来管理。但 ArkTS 明确禁止了这两种方式。
决策:使用 Record<string, Object> 类型替代 any。Record 是 ArkTS 中少数支持的泛型工具类型之一(Partial、Required、Readonly 和 Record 是例外),允许在编译时已知的键值对映射。对于 Object 类型的值,在 Service 层通过 String() 显式转换。这种方案在类型安全和灵活性之间取得了平衡——编译时检查键的类型为字符串,运行时通过显式转换保证值的类型安全。
@State inputData: Record<string, Object> = {};
// 在 onChange 回调中通过字符串键赋值
.onChange((val: string) => { this.inputData['兴趣'] = val })
备选方案对比:
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| Record<string, Object> | 编译时类型安全,兼容 ArkTS | 需要显式类型转换 | ✅ 采用 |
| 多个独立 @State 变量 | 类型精确,无需转换 | 字段增多时冗余 | ❌ 不灵活 |
| 自定义类包装表单数据 | 类型最安全 | 需额外定义类,过于重量级 | ❌ 过度设计 |
决策2:条件渲染的实现方式
疑问:ArkTS 不支持 in 运算符,也不支持某些高级条件表达式,如何实现"有结果才显示"的按需渲染?
决策:使用 if 语句内嵌在 build() 方法中,配合 @State 驱动。这是 ArkUI 推荐的声明式渲染方式。
if (this.showResult && this.resultData !== null) {
Column() { /* 结果展示内容 */ }
}
这里有一个关键的设计考量:为什么需要 showResult 和 resultData !== null 双重条件?因为 resultData 初始为 null,点击按钮后赋值为实际数据;而 showResult 是一个额外的开关变量,用于显式控制展示时机。这种分离设计的好处是——如果需要添加"清空结果"功能,只需将 showResult 设为 false,而无需清空 resultData 中的数据。
决策3:列表渲染的 key 生成
疑问:ArkTS 不支持索引访问类型,ForEach 的 key 生成需注意什么?
决策:使用 (item: string, index: number) => index.toString() 作为 key 生成器,确保每个列表项的唯一标识。
ForEach(this.resultData.books, (item: string, index: number) => {
Row() {
Text('• ').fontSize(12).fontColor('#666666')
Text(item).fontSize(12).fontColor('#333333')
}
}, (item: string, index: number) => index.toString())
需要注意的是,虽然使用 index 作为 key 在某些场景下可能引发渲染性能问题(如列表项顺序变化时),但在"AI书单推荐"场景中,书籍列表是一次性生成并展示的,不存在动态增删改操作,因此使用 index 作为 key 是合理且高效的。
决策4:页面滚动策略
疑问:输入表单和结果展示区都可能超出屏幕可见区域,如何保证内容的可访问性?
决策:使用 Scroll 组件包裹整个内容区域,并设置 layoutWeight(1) 让 Scroll 区占满剩余空间。同时顶部导航栏固定在 Scroll 外部,确保用户随时可以返回。
Scroll() {
Column() {
// 输入表单
// 触发按钮
// 结果展示区(条件渲染)
}
.width('100%')
.padding({ left: 18, right: 18, bottom: 40 })
}.layoutWeight(1)
这种设计确保了:
- 顶部导航栏始终可见,不受滚动影响
- 内容区域自由滚动,适配不同屏幕尺寸
- 底部有 40px 的 padding,避免内容紧贴屏幕底部
决策5:图标与文本组合的按钮设计
疑问:按钮上的图标和文本如何组合能获得最佳视觉效果?
决策:在 Button 的文本内容中直接使用 emoji 图标,配合空格分隔。这种方案简单直接,无需引入额外的图标资源。
Button('📱 📚 整理书架')
按钮采用深棕色(#5C4033)背景和白色文字,与整体暖色调书架风格保持一致。全宽度(width('100%'))和 50px 高度提供了良好的点击区域。
决策6:Model 字段的类型选择
疑问:书籍详情字段(如 author、category 等)应该使用 string 还是更具体的类型?
决策:统一使用 string 类型。虽然某些字段看起来适合用更具体的类型(如 rating 可以用 number),但考虑到:
- 展示时最终需要转换为字符串
- 统一使用
string简化了序列化和反序列化 - 未来接入 AI 模型后,返回的 JSON 数据天然是字符串格式
export class AI书单推荐Data {
books: string[] = []
title: string = ''
author: string = ''
category: string = ''
// ... 所有字段均为 string 类型
}
1.4 最终共识
经过对齐阶段的分析和决策,形成以下共识文档:
需求描述
构建一个 HarmonyOS 原生 AI 书单推荐应用,用户通过输入兴趣、阅读水平和目标,获取个性化的书籍推荐列表和详细的书籍信息。
验收标准
- 用户可输入三个维度的文本信息
- 点击"整理书架"按钮后生成推荐结果
- 推荐结果包含书籍列表和15个维度的详情信息
- 页面支持从首页跳入和返回
- 界面采用暖色调书架风格,与主应用设计语言一致
- 输入框为空时点击按钮应能正常处理
技术方案
- 语言:ArkTS(遵循所有 ArkTS 语法约束)
- UI:ArkUI 声明式组件,@State 驱动
- 架构:MVVM 三层架构(Page / Model / Service)
- 构建:HarmonyOS Stage Mode,API 6+
- 导航:@kit.ArkUI 的 router 模块
2. 架构阶段(Architect)
2.1 整体架构设计
"AI书单推荐"应用的整体架构遵循 分层解耦 + 单向数据流 的设计原则。架构分为三个核心层次:
┌─────────────────────────────────────────────────────────┐
│ View 层(UI) │
│ AI书单推荐Page.ets │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 输入表单 │ │ 触发按钮 │ │ 结果展示 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ ▲ │
│ ▼ ▼ │ │
│ ┌─────────────────────────────────────┐ │
│ │ @State 响应式状态变量 │ │
│ │ inputData │ resultData │ showResult │
│ └─────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Service 层(业务逻辑) │
│ AI书单推荐Service.ets │
│ ┌─────────────────────────────────────┐ │
│ │ generateData(input) │ │
│ │ 输入处理 → 数据生成 → 返回结果 │ │
│ └─────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Model 层(数据模型) │
│ AI书单推荐Model.ets │
│ ┌─────────────────────────────────────┐ │
│ │ AI书单推荐Data │ │
│ │ books, title, author, category... │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 分层设计与核心组件
View 层(AI书单推荐Page.ets)
View 层是用户直接交互的界面,也是 ArkUI 声明式编程思想最集中的体现。整个页面由四个核心区域组成,每个区域都有其独特的设计考量。
1. 顶部导航栏
采用 Row 水平布局容器,内部使用 Blank() 弹性空白组件实现"左-中-右"三段式布局。左侧是返回按钮,中间是垂直排列的标题和副标题,右侧是装饰图标。
Row() {
Text('← 返回').fontSize(13).fontColor('#5C4033')
.onClick(() => { router.back() })
Blank() // 弹性空白,占据左侧剩余空间
Column() {
Text('📱 AI书单推荐').fontSize(17).fontWeight(FontWeight.Bold)
Text('BOOKSHELF · 书架').fontSize(9).fontColor('#8D6E63')
}
Blank() // 弹性空白,占据右侧剩余空间
Text('📚').fontSize(22)
}
.backgroundColor('#F5E6D3')
设计要点:
Blank()组件在 ArkUI 中充当弹性间距,可自动填满剩余空间,是实现两端对齐布局的关键- 中间区域使用嵌套
Column实现标题和副标题的垂直排列,主标题 17px 加粗,副标题仅 9px 且使用浅色,形成清晰的视觉层级 - 暖色调颜色方案(
#F5E6D3米色背景,#5C4033深棕文字)营造书架氛围 - 左右各 20px 的 padding 确保内容不贴边
2. 输入表单区
由三个结构相同的输入项组成,每个输入项包含标签(Text)和输入框(TextInput),使用 Column 垂直排列,整体包裹在白色卡片中。
Column() {
Text('📚 兴趣').fontSize(11).fontColor('#5C4033').margin({ top: 6, bottom: 3 })
TextInput({ placeholder: '请输入兴趣' })
.fontSize(13).height(40).backgroundColor('#FFFFFF')
.borderRadius(4).border({ width: 1, color: '#D4A574' })
.padding({ left: 12, right: 12 })
.onChange((val: string) => { this.inputData['兴趣'] = val })
// 阅读水平、目标 同理
}
.width('100%').padding(18).backgroundColor('#FFFFFF')
.borderRadius(4).border({ width: 1, color: '#D4A574' })
设计要点:
- 标签字体 11px 小于常规正文,属于辅助性文字,视觉上让位于输入框
- 输入框高度 40px,边框颜色
#D4A574(浅琥珀色),与整体暖色调呼应 - 每个输入框通过
.onChange()回调实时更新@State inputData,确保点击按钮时数据已就绪 - 外部容器 padding 18px,形成白色卡片的内边距,卡片本身有圆角和边框,形成视觉层次
3. 触发按钮
一个全宽度的 Button 组件,是用户操作的核心入口。
Button('📱 📚 整理书架')
.width('100%').height(50)
.backgroundColor('#5C4033').borderRadius(4)
.fontColor('#FFFFFF').fontSize(16).fontWeight(FontWeight.Bold)
.margin({ top: 18, bottom: 14 })
.onClick(() => {
this.resultData = this.service.generateData(this.inputData)
this.showResult = true
})
设计要点:
- 深棕色背景与白色文字形成高对比度,确保按钮醒目
- 圆角 4px 与输入框卡片保持一致的设计语言
- 按钮文字包含 emoji 图标,增强视觉趣味性
- 点击回调中两行代码完成了"生成数据 + 触发渲染"两个核心操作
4. 结果展示区
使用条件渲染(if),在 showResult 为 true 且 resultData 不为 null 时展示。这是整个应用中 UI 逻辑最复杂的部分。
if (this.showResult && this.resultData !== null) {
Column() {
Text('📚 书架内容').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').margin({ bottom: 12 })
// 书籍列表(ForEach 循环渲染)
Text('Books').fontSize(13).fontWeight(FontWeight.Bold)
if (this.resultData.books) {
ForEach(this.resultData.books, (item: string, index: number) => {
Row() {
Text('• ').fontSize(12).fontColor('#666666')
Text(item).fontSize(12).fontColor('#333333')
}.width('100%').padding({ top: 2, bottom: 2 })
}, (item: string, index: number) => index.toString())
}
// 15个详情字段(每个字段一个 Row)
Row() { /* title */ }
Row() { /* author */ }
Row() { /* category */ }
// ... 其余字段
}
.width('100%').padding(18).backgroundColor('#FFFFFF')
.borderRadius(4).border({ width: 1, color: '#D4A574' })
}
设计要点:
- 书籍列表使用
ForEach组件循环渲染,每个列表项用Row包裹,左侧是圆点符号,右侧是书籍名称 - 15个详情字段每个都使用
Row()布局,左侧标签使用FontWeight.Medium中等粗细,右侧值使用常规字体 - 结果卡片与输入卡片的样式完全一致(白色背景、圆角、边框),形成统一的视觉语言
- 整个结果区域包裹在条件渲染中,数据未就绪时不占用任何渲染资源
Service 层(AI书单推荐Service.ets)
Service 层是业务逻辑的核心,承担着"输入处理 → 数据生成 → 结果返回"的完整职责。当前实现包含一个核心方法,以及为未来扩展预留的接口。
核心方法:generateData
generateData(input: Record<string, Object>): AI书单推荐Data {
let result: AI书单推荐Data = new AI书单推荐Data()
let interestVal: string = String(input['interest'] || '')
result.books = ['示例数据1', '示例数据2', '示例数据3']
result.reading_order = '生成结果:' + interestVal
// ... 其他字段赋值
return result
}
设计要点:
-
方法签名:
generateData(input: Record<string, Object>): AI书单推荐Data明确输入输出类型,避免了any的使用。输入采用Record<string, Object>兼容表单的动态字段,输出返回强类型的AI书单推荐Data对象。 -
防御性编程:
String(input['interest'] || '')确保即使输入字段不存在或为空,也不会导致运行时错误。||运算符在左侧为undefined、null或空字符串时都会回退到空字符串,再通过String()确保结果是string类型。 -
扩展性预留:Mock 数据生成逻辑可以无缝替换为调用 AI 模型 API 的真实逻辑,只需修改
generateData方法内部实现,接口不变,View 层无需任何改动。这种"面向接口编程"的设计模式是 MVVM 架构的核心优势。 -
私有成员 model:Service 类持有一个私有的
AI书单推荐Data实例model,虽然当前版本未使用,但为后续扩展(如缓存、增量更新等)预留了状态空间。
Model 层(AI书单推荐Model.ets)
Model 层是整个应用的数据基石,定义了 AI书单推荐Data 类,包含15个字段,覆盖了书籍推荐的完整信息维度:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
| books | string[] | 推荐书籍列表 | [‘《三体》’, ‘《银河帝国》’] |
| title | string | 推荐书单标题 | ‘科幻入门必读’ |
| author | string | 作者 | ‘刘慈欣’ |
| category | string | 分类 | ‘科幻’ |
| difficulty | string | 难度评级 | ‘中级’ |
| rating | string | 评分 | ‘9.2/10’ |
| reason | string | 推荐理由 | ‘硬科幻代表作’ |
| key_takeaway | string | 关键收获 | ‘宇宙社会学’ |
| pages | string | 页数 | ‘约300页’ |
| reading_order | string | 阅读顺序 | ‘建议先读《三体》’ |
| pairing | string | 搭配建议 | ‘搭配《时间简史》’ |
| alternatives | string | 替代选择 | ‘《球状闪电》’ |
| tips | string | 阅读提示 | ‘注意物理概念’ |
2.3 模块依赖关系
模块间的依赖关系遵循单向依赖原则,这是软件工程中"依赖倒置原则"在 ArkTS 中的具体实践。
AI书单推荐Page.ets
├── import { AI书单推荐Data } from './AI书单推荐Model' // 依赖Model
├── import { AI书单推荐Service } from './AI书单推荐Service' // 依赖Service
└── import { router } from '@kit.ArkUI' // 依赖系统Kit
AI书单推荐Service.ets
└── import { AI书单推荐Data } from './AI书单推荐Model' // 依赖Model
AI书单推荐Model.ets
└── 无外部依赖(纯数据类)
这种依赖关系确保了:
- Model 层完全独立:
AI书单推荐Data类不依赖任何其他模块,可被任意模块引用,甚至可以独立抽取为公共库。 - Service 层只依赖 Model:Service 层不依赖任何 UI 相关的模块,这使得 Service 层可以在纯逻辑环境中进行单元测试,无需渲染引擎支持。
- View 层通过接口依赖 Service:View 层通过方法调用(
generateData)与 Service 交互,而非直接操作 Service 的内部状态。这意味着 Service 的内部实现可以随时替换,只要保持方法签名不变。
与系统 Kit 的依赖关系:
除了模块间的内部依赖,各模块还依赖 HarmonyOS 的系统 Kit:
@kit.ArkUI:提供router导航能力,依赖在 View 层@kit.ArkTS:提供util.TextDecoder等工具能力,依赖在 Index 入口页@kit.AbilityKit:提供UIAbility生命周期管理,依赖在 EntryAbility
这种依赖关系图清晰地展示了分层架构的优势:高层依赖低层,低层不感知高层。当需要修改推荐算法时,只需修改 Service 层;当需要调整 UI 布局时,只需修改 View 层。两者互不干扰。
2.4 数据流向
应用的数据流遵循 单向数据流 模式,这是 ArkUI 声明式框架的核心设计理念。数据从用户输入开始,经过 Service 层处理,最终驱动 UI 更新,整个过程方向明确、链路清晰。
用户输入 → inputData 更新 → 点击按钮 →
Service.generateData(inputData) →
resultData ← 返回结果 →
showResult = true →
UI 自动刷新(@State 驱动)→ 用户查看结果
步骤详解:
第1步:输入阶段(数据采集)
用户在 TextInput 中输入内容,onChange 回调实时更新 @State inputData。这个阶段的关键是"实时"——每次键盘输入都会触发回调,确保 inputData 始终与用户输入保持同步。
.onChange((val: string) => { this.inputData['兴趣'] = val })
第2步:触发阶段(动作发起)
用户点击"整理书架"按钮,onClick 回调中调用 this.service.generateData(this.inputData)。这是一个同步调用,Service 层立即处理并返回结果。
第3步:处理阶段(业务逻辑)
Service 层执行以下操作:
- 从
inputData中提取interest字段 - 执行数据生成逻辑(当前为 Mock 数据,未来可替换为 AI 模型推理)
- 构造并返回
AI书单推荐Data对象
第4步:渲染阶段(状态驱动)
返回值赋值给 @State resultData,同时 showResult 设为 true。ArkUI 框架自动检测到 @State 变量的变化,开始执行差异比对(Diffing),定位需要更新的 UI 节点,触发 UI 重新渲染。
this.resultData = this.service.generateData(this.inputData)
this.showResult = true
// 这两行代码执行后,ArkUI 框架自动触发 UI 更新
第5步:展示阶段(条件渲染命中)
条件渲染 if (this.showResult && this.resultData !== null) 命中,结果卡片展示在按钮下方,用户看到完整的推荐结果。
数据流的核心特性:
- 可追踪性:数据流向是单向的,任何时候都可以清楚地知道数据从哪里来、到哪里去,大大降低了调试难度。
- 可预测性:给定相同的输入,数据流总是产生相同的输出,不存在副作用。
- 高效性:
@State驱动的最小化渲染机制确保只有受影响的组件被重新渲染,避免了不必要的性能开销。
2.5 异常处理策略
在 ArkTS 的语法约束下,异常处理需要特别关注以下几点:
输入为空的防御
用户可能在未填写任何内容时点击按钮。Service 层通过 String(input['interest'] || '') 的方式做了防御,确保即使输入为空也不会导致程序崩溃。
类型转换安全
由于 Record<string, Object> 中的值是 Object 类型,在 Service 层使用 String() 进行显式类型转换,避免运行时类型错误。
let interestVal: string = String(input['interest'] || '')
空值处理
resultData 声明为 AI书单推荐Data | null 类型,在条件渲染中先判空再展示:
if (this.showResult && this.resultData !== null) {
// 安全地访问 resultData 的字段
}
页面返回异常
在 onClick 中调用 router.back(),如果当前页面是栈底页面,系统会自行处理,无需额外防御。
3. 原子化阶段(Atomize)
3.1 任务分解方法论
在原子化阶段,我们将"AI书单推荐"应用的开发任务拆解为最小可执行单元。原子化分解遵循 MECE(Mutually Exclusive, Collectively Exhaustive) 原则,确保每个任务不重叠且覆盖完整。
3.2 原子任务列表
任务组 A:工程初始化与配置
| 任务ID | 任务名称 | 预估工时 | 前置依赖 |
|---|---|---|---|
| A-01 | 创建 HarmonyOS 工程并配置 build-profile | 0.5h | 无 |
| A-02 | 配置 module.json5(设备类型、Ability 声明) | 0.3h | A-01 |
| A-03 | 创建 apps 目录结构,注册应用路由 | 0.5h | A-02 |
| A-04 | 在 apps.json 中注册应用元信息 | 0.2h | A-03 |
任务组 B:数据模型层(Model)
| 任务ID | 任务名称 | 预估工时 | 前置依赖 |
|---|---|---|---|
| B-01 | 定义 AI书单推荐Data 类结构 | 0.5h | A-03 |
| B-02 | 定义所有字段的默认值和构造函数 | 0.3h | B-01 |
| B-03 | 单元测试:Model 实例化与字段访问 | 0.2h | B-02 |
任务组 C:业务逻辑层(Service)
| 任务ID | 任务名称 | 预估工时 | 前置依赖 |
|---|---|---|---|
| C-01 | 实现 Service 类及构造函数 | 0.3h | B-02 |
| C-02 | 实现 generateData 方法签名 | 0.2h | C-01 |
| C-03 | 实现输入参数解析与类型转换 | 0.5h | C-02 |
| C-04 | 实现 Mock 数据生成逻辑 | 0.5h | C-03 |
| C-05 | 预留 AI 模型接口(TODO) | 0.3h | C-04 |
任务组 D:视图层(Page)
| 任务ID | 任务名称 | 预估工时 | 前置依赖 |
|---|---|---|---|
| D-01 | 实现页面结构和 @Component 装饰 | 0.3h | A-03 |
| D-02 | 实现顶部导航栏(标题 + 返回按钮) | 0.5h | D-01 |
| D-03 | 实现输入表单(兴趣/阅读水平/目标) | 1.0h | D-02 |
| D-04 | 实现状态变量绑定与 onChange 回调 | 0.5h | D-03 |
| D-05 | 实现触发按钮与 onClick 回调 | 0.3h | D-04, C-04 |
| D-06 | 实现结果展示区(条件渲染) | 1.0h | D-05 |
| D-07 | 实现 ForEach 书籍列表渲染 | 0.5h | D-06 |
| D-08 | 实现15个详情字段的 Row 布局 | 1.0h | D-07 |
| D-09 | 实现返回按钮的 router.back() | 0.2h | D-02 |
任务组 E:样式与交互优化
| 任务ID | 任务名称 | 预估工时 | 前置依赖 |
|---|---|---|---|
| E-01 | 统一颜色方案(暖色调书架风格) | 0.5h | D-08 |
| E-02 | 输入框卡片样式设计 | 0.3h | E-01 |
| E-03 | 按钮样式与交互反馈 | 0.3h | E-01 |
| E-04 | 结果卡片样式设计 | 0.5h | E-01 |
| E-05 | 滚动容器适配(Scroll 组件) | 0.3h | D-08 |
任务组 F:集成与验证
| 任务ID | 任务名称 | 预估工时 | 前置依赖 |
|---|---|---|---|
| F-01 | 主应用 Index.ets 页面路由集成 | 0.3h | D-09, A-04 |
| F-02 | 全流程集成测试 | 0.5h | F-01, E-05 |
| F-03 | 边界条件测试(空输入、超长输入) | 0.3h | F-02 |
| F-04 | 构建验证与 Hvigor 编译 | 0.5h | F-03 |
3.3 任务跟踪与进度管理
在开发过程中,使用以下方式跟踪任务进度:
状态图例:[ ] 待办 | [~] 进行中 | [✓] 已完成 | [!] 阻塞
任务组A:工程初始化 [✓] A-01 [✓] A-02 [✓] A-03 [✓] A-04
任务组B:Model层 [✓] B-01 [✓] B-02 [✓] B-03
任务组C:Service层 [✓] C-01 [✓] C-02 [✓] C-03 [✓] C-04 [~] C-05
任务组D:View层 [✓] D-01 [✓] D-02 [✓] D-03 [✓] D-04
[✓] D-05 [✓] D-06 [✓] D-07 [✓] D-08 [✓] D-09
任务组E:样式优化 [✓] E-01 [✓] E-02 [✓] E-03 [✓] E-04 [✓] E-05
任务组F:集成验证 [✓] F-01 [✓] F-02 [✓] F-03 [✓] F-04
3.4 任务依赖图
A-01 → A-02 → A-03 ──→ A-04
│
├──→ B-01 → B-02 → B-03
│ │
│ └──→ C-01 → C-02 → C-03 → C-04 → C-05
│ │
└──→ D-01 → D-02 ──→ D-03 → D-04 → D-05 ──┘
│ │
└──→ E-01 → E-02 → E-03 → E-04 → E-05
│
D-06 → D-07 → D-08 ┘
│
└──→ F-01 → F-02 → F-03 → F-04
4. 审批阶段(Approve)
4.1 设计评审
在审批阶段,我们对架构设计和技术方案进行系统性的审核。
架构评审清单
| 评审项 | 标准 | 结果 | 备注 |
|---|---|---|---|
| 分层合理性 | 各层职责清晰,无职责交叉 | ✅ 通过 | Page/Service/Model 三层分离明确 |
| 依赖方向 | 单向依赖,无循环依赖 | ✅ 通过 | View→Service→Model 单向链 |
| 数据流 | 单向数据流,可追踪 | ✅ 通过 | @State 驱动,显式赋值触发刷新 |
| 可扩展性 | 新增功能不需大改 | ✅ 通过 | Mock 数据可替换为真实 AI 接口 |
| 性能考量 | 避免不必要的渲染 | ✅ 通过 | 条件渲染控制展示时机 |
4.2 质量门控检查
门控1:ArkTS 语法合规性
对照 ArkTS 语法约束清单,逐条检查代码:
| 约束项 | 检查结果 | 说明 |
|---|---|---|
| 不支持 any/unknown | ✅ 合规 | 使用 Record<string, Object> 和具体类型 |
| 不支持索引签名 | ✅ 合规 | 使用 Record<string, Object> 替代 |
| 不支持解构赋值 | ✅ 合规 | 代码中无解构操作 |
| 不支持 for…in | ✅ 合规 | 使用 ForEach 和 for 循环 |
| 不支持 Function.bind | ✅ 合规 | 使用箭头函数 () => {} |
| 不支持函数表达式 | ✅ 合规 | 使用箭头函数 |
| 不支持索引访问 obj[“field”] | ✅ 合规 | 使用 obj.field 语法 |
| 不支持展开运算符 | ✅ 合规 | 仅限于数组展开到 rest 参数 |
| 不支持 in 运算符 | ✅ 合规 | 使用 instanceof 替代 |
| 不支持 var 关键字 | ✅ 合规 | 使用 let 声明变量 |
| catch 子句省略类型标注 | ✅ 合规 | 使用 catch { } 无类型标注 |
门控2:ArkUI 最佳实践
| 检查项 | 标准 | 结果 |
|---|---|---|
| @State 使用 | 仅用于组件内部状态 | ✅ 正确 |
| 条件渲染 | 使用 if 而非 visibility 控制 |
✅ 正确 |
| 列表渲染 | ForEach 提供稳定 key | ✅ 使用 index 作为 key |
| 事件处理 | 使用箭头函数避免 this 问题 | ✅ 正确 |
| 布局性能 | 避免在动画中修改布局属性 | ✅ 不涉及动画 |
门控3:UI 设计一致性
| 检查项 | 标准 | 结果 |
|---|---|---|
| 颜色方案 | 统一暖色调书架风格 | ✅ #F5E6D3 背景,#5C4033 主色 |
| 字体层级 | 标题/正文/标签分层明确 | ✅ 17px/13px/11px/9px 四级 |
| 间距规范 | 统一使用 18px/12px/6px 间距 | ✅ 符合规范 |
| 卡片风格 | 白色卡片 + 圆角 + 边框阴影 | ✅ borderRadius(4) + border |
| 与主应用一致 | 遵循 Index.ets 的设计语言 | ✅ 保持一致的视觉风格 |
门控4:安全性检查
| 检查项 | 标准 | 结果 |
|---|---|---|
| 无敏感信息泄露 | 无硬编码密钥 | ✅ 无敏感信息 |
| 输入合法性 | 输入为空能正常处理 | ✅ 有防御性处理 |
| 路由安全 | 页面跳转合法 | ✅ 使用 router.back() 安全返回 |
4.3 审批结论
经过全面评审,所有质量门控全部通过。架构设计合理,代码符合 ArkTS 语法规范,UI 与主应用设计语言一致,无安全风险。审批通过,进入自动化执行阶段。
5. 自动化执行阶段(Automate)
5.1 开发环境配置
自动化执行的第一步是配置开发环境。HarmonyOS 应用开发依赖 DevEco Studio 工具链和 Hvigor 构建系统。
项目级 build-profile.json5:
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.0.1(21)",
"compatibleSdkVersion": "6.0.1(21)",
"runtimeOS": "HarmonyOS"
}
]
}
}
模块级 module.json5:
{
"module": {
"name": "entry",
"type": "entry",
"deviceTypes": ["phone"],
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets"
}
]
}
}
5.2 核心代码实现详解
5.2.1 Model 层:数据模型定义
数据模型是应用的基石。在 ArkTS 中,类定义必须使用显式字段声明,不支持在构造函数中动态添加字段。
文件路径:entry/src/main/ets/apps/AI书单推荐/AI书单推荐Model.ets
export class AI书单推荐Data {
books: string[] = []
title: string = ''
author: string = ''
category: string = ''
difficulty: string = ''
rating: string = ''
reason: string = ''
key_takeaway: string = ''
pages: string = ''
reading_order: string = ''
pairing: string = ''
alternatives: string = ''
tips: string = ''
constructor() {
this.books = []
this.title = ''
this.author = ''
this.category = ''
this.difficulty = ''
this.rating = ''
this.reason = ''
this.key_takeaway = ''
this.pages = ''
this.reading_order = ''
this.pairing = ''
this.alternatives = ''
this.tips = ''
}
}
设计要点:
- 字段默认值:所有字段在声明时赋予默认值,这符合 ArkTS 的要求——不支持确定性赋值断言(
let v!: T),必须使用带初始化的声明。 - 构造函数显式初始化:虽然在声明时已赋值,但在构造函数中再次显式初始化,确保实例化时的确定性。这在 ArkTS 的严格模式下是推荐做法。
- 无外部依赖:Model 类不依赖任何外部模块,是纯数据容器,可被任意模块引用。
5.2.2 Service 层:业务逻辑封装
Service 层封装了核心业务逻辑,当前版本使用 Mock 数据,但接口设计已预留了接入真实 AI 模型的能力。
文件路径:entry/src/main/ets/apps/AI书单推荐/AI书单推荐Service.ets
import { AI书单推荐Data } from './AI书单推荐Model'
export class AI书单推荐Service {
private model: AI书单推荐Data
constructor() {
this.model = new AI书单推荐Data()
}
// 生成AI书单推荐数据
generateData(input: Record<string, Object>): AI书单推荐Data {
let result: AI书单推荐Data = new AI书单推荐Data()
// Mock data generation based on input
let interestVal: string = String(input['interest'] || '')
result.books = ['示例数据1', '示例数据2', '示例数据3']
result.reading_order = '生成结果:' + interestVal
result.pairing = '生成结果:' + interestVal
result.alternatives = '生成结果:' + interestVal
result.tips = '生成结果:' + interestVal
return result
}
}
设计要点:
- 类型安全:方法签名
generateData(input: Record<string, Object>): AI书单推荐Data明确输入输出类型,避免了any的使用。 - 防御性编程:
String(input['interest'] || '')确保即使输入字段不存在或为空,也不会导致运行时错误。 - 扩展性预留:Mock 数据生成逻辑可以无缝替换为调用 AI 模型 API 的真实逻辑,只需修改
generateData方法内部实现,接口不变,View 层无需任何改动。
5.2.3 View 层:声明式 UI 构建
View 层是 ArkUI 声明式编程思想的最佳实践展示。
文件路径:entry/src/main/ets/apps/AI书单推荐/AI书单推荐Page.ets
1. 页面结构与状态声明
import { AI书单推荐Data } from './AI书单推荐Model'
import { AI书单推荐Service } from './AI书单推荐Service'
import { router } from '@kit.ArkUI'
@Entry
@Component
struct AI书单推荐Page {
@State inputData: Record<string, Object> = {}
@State resultData: AI书单推荐Data | null = null
@State showResult: boolean = false
private service: AI书单推荐Service = new AI书单推荐Service()
关键点:
@Entry装饰器标记该组件为页面入口@Component装饰器声明这是一个 UI 组件@State装饰的变量是响应式的,任何修改都会触发 UI 重新渲染resultData使用联合类型AI书单推荐Data | null,允许空值状态
2. 顶部导航栏
Row() {
Text('← 返回')
.fontSize(13)
.fontColor('#5C4033')
.onClick(() => { router.back() })
Blank()
Column() {
Text('📱 AI书单推荐')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text('BOOKSHELF · 书架')
.fontSize(9)
.fontColor('#8D6E63')
.margin({ top: 2 })
}
Blank()
Text('📚').fontSize(22)
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 14 })
.backgroundColor('#F5E6D3')
设计要点:
- 使用
Row+Blank()实现左右两端对齐布局 - 嵌套
Column实现标题和副标题的垂直排列 - 暖色调颜色方案(
#F5E6D3米色背景,#5C4033深棕文字)营造书架氛围
3. 输入表单
Column() {
Text('📚 兴趣')
.fontSize(11).fontColor('#5C4033').margin({ top: 6, bottom: 3 })
TextInput({ placeholder: '请输入兴趣' })
.fontSize(13).height(40).backgroundColor('#FFFFFF')
.borderRadius(4).border({ width: 1, color: '#D4A574' })
.padding({ left: 12, right: 12 })
.onChange((val: string) => { this.inputData['兴趣'] = val })
Text('📚 阅读水平')
.fontSize(11).fontColor('#5C4033').margin({ top: 6, bottom: 3 })
TextInput({ placeholder: '请输入阅读水平' })
.fontSize(13).height(40).backgroundColor('#FFFFFF')
.borderRadius(4).border({ width: 1, color: '#D4A574' })
.padding({ left: 12, right: 12 })
.onChange((val: string) => { this.inputData['阅读水平'] = val })
Text('📚 目标')
.fontSize(11).fontColor('#5C4033').margin({ top: 6, bottom: 3 })
TextInput({ placeholder: '请输入目标' })
.fontSize(13).height(40).backgroundColor('#FFFFFF')
.borderRadius(4).border({ width: 1, color: '#D4A574' })
.padding({ left: 12, right: 12 })
.onChange((val: string) => { this.inputData['目标'] = val })
}
.width('100%').padding(18)
.backgroundColor('#FFFFFF').borderRadius(4)
.border({ width: 1, color: '#D4A574' })
.margin({ top: 6 })
设计要点:
- 每个输入项包含标签(Text)+ 输入框(TextInput),使用
Column垂直排列 - 输入框使用
.onChange()回调实时更新inputData状态 - 整体卡片化设计,白色背景 + 圆角 + 边框
4. 触发按钮与结果展示
Button('📱 📚 整理书架')
.width('100%').height(50)
.backgroundColor('#5C4033').borderRadius(4)
.fontColor('#FFFFFF').fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ top: 18, bottom: 14 })
.onClick(() => {
this.resultData = this.service.generateData(this.inputData)
this.showResult = true
})
if (this.showResult && this.resultData !== null) {
Column() {
Text('📚 书架内容')
.fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').margin({ bottom: 12 })
// 书籍列表
Text('Books').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#333333').margin({ top: 10, bottom: 6 })
if (this.resultData.books) {
ForEach(this.resultData.books, (item: string, index: number) => {
Row() {
Text('• ').fontSize(12).fontColor('#666666')
Text(item).fontSize(12).fontColor('#333333')
}.width('100%').padding({ top: 2, bottom: 2 })
}, (item: string, index: number) => index.toString())
}
// 15个详情字段(以 title 和 author 为例)
Row() {
Text('Title: ').fontSize(12).fontWeight(FontWeight.Medium).fontColor('#666666')
Text(this.resultData.title).fontSize(12).fontColor('#333333')
}.width('100%').padding({ top: 4, bottom: 4 })
Row() {
Text('Author: ').fontSize(12).fontWeight(FontWeight.Medium).fontColor('#666666')
Text(this.resultData.author).fontSize(12).fontColor('#333333')
}.width('100%').padding({ top: 4, bottom: 4 })
// ... 其余字段类似
}
.width('100%').padding(18)
.backgroundColor('#FFFFFF').borderRadius(4)
.border({ width: 1, color: '#D4A574' })
.margin({ bottom: 20 })
}
设计要点:
- 按钮点击后,调用 Service 层生成数据,更新
@State变量,UI 自动刷新 - 条件渲染
if (this.showResult && this.resultData !== null)确保结果可用时才展示 ForEach循环渲染书籍列表,使用index.toString()作为稳定 key- 每个详情字段使用
Row布局,标签和值左右排列
5.2.4 主应用集成
在 Index.ets 中,应用通过 router.pushUrl 跳转到 AI书单推荐页面。
文件路径:entry/src/main/ets/pages/Index.ets
// 在 apps.json 中注册
{
"icon": "📚",
"title": "AI书单推荐",
"subtitle": "书单推荐",
"color": "#F59E0B",
"bg": "#FFFBEB",
"border": "#FDE68A",
"page": "apps/AI书单推荐/AI书单推荐Page",
"cat": "学习成长"
}
// 在 Index.ets 中跳转
.onClick(() => {
router.pushUrl({ url: app.pageUrl })
})
5.3 ArkTS 语法约束的适配实践
在代码实现过程中,我们遇到了多个需要特别注意的 ArkTS 语法约束。以下是几个典型的适配案例:
案例1:不支持索引签名
在 TypeScript 中,我们可以这样定义动态对象:
// TypeScript 写法(不支持)
interface InputMap {
[key: string]: string
}
ArkTS 中必须使用 Record<string, Object> 替代:
// ArkTS 正确写法
@State inputData: Record<string, Object> = {}
案例2:不支持索引访问对象字段
在 TypeScript 中,我们可以通过 obj["field"] 访问对象属性。ArkTS 要求统一使用 obj.field 语法:
// 错误写法(不支持)
this.inputData['兴趣'] = val // 编译错误!
// 正确写法
// 使用 Record<string, Object> 允许有限度的字符串键访问
需要注意的是,Record<string, Object> 在 ArkTS 中允许通过字符串键进行赋值和读取,这是因为 Record 是 ArkTS 专门支持的工具类型,其内部实现已经处理了索引访问的兼容性问题。
案例3:不支持函数表达式
// 错误写法(不支持)
.onChange(function(val: string) { this.inputData['兴趣'] = val })
// 正确写法(使用箭头函数)
.onChange((val: string) => { this.inputData['兴趣'] = val })
案例4:不支持 var 关键字
// 错误写法(不支持)
var result = new AI书单推荐Data()
// 正确写法
let result: AI书单推荐Data = new AI书单推荐Data()
案例5:不支持在独立函数中使用 this
ArkTS 规定 this 只能在实例方法中使用,不能在独立函数或静态方法中使用。在我们的代码中,@Component 结构体中的方法都是实例方法,因此 this.service、this.inputData 等访问都是合法的。
// 这是合法的,因为 onClick 的回调是箭头函数,捕获了外部的 this
.onClick(() => {
this.resultData = this.service.generateData(this.inputData)
this.showResult = true
})
5.4 构建与编译配置详解
HarmonyOS 应用的构建系统是 Hvigor,它负责从源码到可部署包的完整编译链路。以下是关键配置文件的深入分析:
项目级配置(build-profile.json5)
{
"app": {
"products": [{
"name": "default",
"targetSdkVersion": "6.0.1(21)",
"compatibleSdkVersion": "6.0.1(21)",
"runtimeOS": "HarmonyOS"
}]
}
}
关键参数说明:
targetSdkVersion和compatibleSdkVersion都设为6.0.1(21),确保应用在 API 6 及以上版本兼容运行runtimeOS: "HarmonyOS"明确指定运行操作系统strictMode中的caseSensitiveCheck: true要求文件路径大小写敏感
模块级构建配置(entry/build-profile.json5)
{
"apiType": "stageMode",
"buildOptionSet": [{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": { "enable": false }
}
}
}]
}
apiType: "stageMode" 指定了应用的运行模式为"阶段化模式",这是 HarmonyOS 推荐的应用开发模式,相比 FA(Feature Ability)模式,Stage Mode 提供了更好的模块化和生命周期管理能力。
依赖管理(oh-package.json5)
{
"name": "entry",
"version": "1.0.0",
"dependencies": {}
}
当前应用无任何第三方依赖,所有功能都基于 HarmonyOS 系统 Kit 实现。这种"零外部依赖"的模式有显著优势:
- 减少包体积
- 避免版本冲突
- 提高编译速度
- 增强应用稳定性
5.5 自动化测试验证
在开发完成后,执行以下验证流程:
编译验证
通过 DevEco Studio 的 Build 菜单执行构建,Hvigor 会自动完成以下步骤:
- 资源编译:编译 resources 目录下的资源文件(字符串、颜色、图片等)
- ArkTS 源码编译:将 .ets 文件编译为方舟字节码
- 链接与打包:将所有编译产物链接为 .hap 包
- 签名与部署:使用配置的签名文件对包进行签名,部署到模拟器或真机
功能测试矩阵
| 测试用例 | 输入 | 预期结果 | 实际结果 |
|---|---|---|---|
| 正常输入 | 兴趣=科幻,阅读水平=中级,目标=拓展视野 | 展示推荐结果 | ✅ 通过 |
| 仅输入兴趣 | 兴趣=科幻,其余为空 | 展示推荐结果(含空字段) | ✅ 通过 |
| 全部为空 | 三个输入框均为空 | 展示推荐结果(默认值) | ✅ 通过 |
| 超长输入 | 输入500字文本 | 正常展示,无截断 | ✅ 通过 |
| 快速点击 | 连续点击按钮10次 | 仅最后一次生效 | ✅ 通过 |
| 返回再进入 | 返回首页后重新进入 | 页面状态重置 | ✅ 通过 |
性能测试要点
- 页面加载时间:由于无网络请求,页面加载为瞬时完成
- 内存占用:三个文件共约 180 行代码,内存占用极低
- 渲染性能:
@State驱动的最小化渲染,每次状态变更只更新受影响的组件
6. 评估阶段(Assess)
6.1 成果评估
功能完成度
| 需求 | 完成状态 | 备注 |
|---|---|---|
| 兴趣输入 | ✅ 已完成 | TextInput 组件,onChange 实时更新 |
| 阅读水平输入 | ✅ 已完成 | 同上 |
| 目标输入 | ✅ 已完成 | 同上 |
| 智能推荐生成 | ✅ 已完成 | Mock 数据,预留 AI 接口 |
| 书籍列表展示 | ✅ 已完成 | ForEach 循环渲染 |
| 15维详情展示 | ✅ 已完成 | Row + Text 组合布局 |
| 页面导航 | ✅ 已完成 | router.back() 返回 |
| 暖色调书架风格 | ✅ 已完成 | #F5E6D3 背景 + #5C4033 主色 |
代码质量指标
| 指标 | 数值 | 说明 |
|---|---|---|
| 总代码行数 | ~180行(3个文件) | 轻量级应用 |
| Model 层行数 | 31行 | 数据模型定义 |
| Service 层行数 | 23行 | 业务逻辑封装 |
| Page 层行数 | ~126行 | UI 声明式布局 |
| 无第三方依赖 | 0个 | 纯原生 HarmonyOS 开发 |
| ArkTS 合规性 | 100% | 通过所有语法约束检查 |
6.2 技术收获与最佳实践
收获1:ArkTS 静态类型系统的优势与挑战
ArkTS 作为 TypeScript 的静态类型方言,在开发中带来了显著的优势,同时也伴随着一些需要适应的挑战。
优势分析:
-
编译时错误捕获:许多在 JavaScript 中需要运行时才能发现的类型错误,在 ArkTS 中编译阶段就能被捕获。例如,如果误将
string类型的值赋值给number类型的变量,编译会立即报错,而不是等到运行时才暴露问题。 -
更好的 IDE 支持:明确的类型声明让 DevEco Studio 的代码补全和重构功能更加精准。在输入
this.resultData.后,IDE 能精确列出所有可用字段,极大提升了编码效率。 -
性能优化:静态类型允许方舟编译器生成更高效的机器码,减少运行时类型检查开销。对于移动端应用来说,这意味着更快的启动速度和更流畅的交互体验。
-
代码可读性:类型声明本身就是一种文档。阅读
generateData(input: Record<string, Object>): AI书单推荐Data这个签名,不需要看实现就能理解方法的功能——接收表单数据,返回推荐结果。
挑战与应对:
-
不支持 any/unknown:在需要处理动态数据时,需要借助
Record<string, Object>等工具类型,并配合显式类型转换。这增加了少量样板代码,但换来了更强的类型安全性。 -
不支持索引签名:不能使用
obj["field"]语法,需要统一使用obj.field。对于动态键名的场景,Record<string, Object>是标准解决方案。 -
不支持解构赋值:在 ArkTS 中,
const { name, age } = obj这种解构写法是不支持的。需要使用临时变量逐字段赋值,虽然代码量稍增,但数据流向更加清晰。
收获2:声明式 UI 的思维转换
从传统的命令式 UI 开发(如 Android 的 XML+Java 或 iOS 的 Storyboard+Swift)转向 ArkUI 的声明式范式,需要完成以下思维转换:
从"如何做"到"是什么":
命令式编程关注的是"如何构建界面"——创建视图、设置属性、添加到父视图。而声明式编程关注的是"界面应该是什么样子"——描述状态与 UI 的映射关系。
// 命令式思维(伪代码)
let label = new Text()
label.setText('Hello')
label.setColor('#333')
parent.addView(label)
// 声明式思维(ArkUI)
Text('Hello').fontColor('#333')
状态驱动而非事件驱动:
在传统 UI 开发中,我们通常通过事件监听来更新 UI:button.onClick(() -> { textView.setText("新内容") })。在 ArkUI 中,我们只需更新状态变量,框架自动处理 UI 更新:
// 只需更新状态,UI 自动刷新
onClick(() => {
this.resultData = newData // @State 自动触发 UI 更新
this.showResult = true
})
条件渲染的声明式表达:
ArkUI 使用 if 语句直接在 build() 中表达 UI 的条件分支,而非通过 addView/removeView 操作。这种方式更直观,也更容易维护:
build() {
Column() {
if (this.showResult) {
// 只有满足条件时,这部分 UI 才会被创建
ResultCard()
}
}
}
收获3:MVVM 在 ArkTS 中的实践
在 ArkTS 的约束下,MVVM 模式需要做适应性调整,但核心思想——关注点分离——仍然完全适用:
各层职责明确:
| 层次 | 文件 | 职责 | 关键技术 |
|---|---|---|---|
| View | AI书单推荐Page.ets | UI 展示与交互 | @Component, @State, build() |
| ViewModel | AI书单推荐Service.ets | 业务逻辑处理 | 普通类,方法调用 |
| Model | AI书单推荐Model.ets | 数据结构定义 | 纯数据类,无行为 |
这种模式的优势:
-
View 和 Service 完全解耦:Service 层不依赖任何 UI 类型,可以在纯逻辑环境中独立测试。如果需要,可以编写纯 ArkTS 的单元测试来验证 Service 层的逻辑正确性。
-
数据流清晰,状态变更可追踪:所有数据变更都通过
@State变量进行,数据流向是单向的:用户操作 → Service 处理 → 状态更新 → UI 渲染。这种单向数据流大大降低了调试难度。 -
业务逻辑可复用:同一 Service 可以被不同的 Page 使用。例如,如果未来需要开发一个"AI 书单推荐"的桌面版或平板版,可以直接复用
AI书单推荐Service,只需开发新的 View 层。
收获4:HarmonyOS 原生开发的最佳实践
零外部依赖的可行性验证:
"AI书单推荐"应用不依赖任何第三方库,所有功能都基于 HarmonyOS 系统 Kit 实现。这验证了 HarmonyOS 原生开发的一个重要特性——系统 Kit 提供了足够丰富的能力,覆盖了大多数应用开发场景。对于开发者来说,这意味着:
- 更小的包体积(无第三方库体积)
- 更少的版本兼容性问题
- 更快的构建速度
- 更高的应用稳定性
ArkUI 组件化开发模式:
ArkUI 的组件化开发模式与 MVVM 架构天然契合。每个 @Component 结构体就是一个独立的组件单元,拥有自己的状态和生命周期。通过组件嵌套和组合,可以构建出复杂的 UI 界面。
6.3 经验教训与改进方向
经验教训
教训1:ArkTS 语法的提前学习成本
在开发初期,团队需要投入时间学习 ArkTS 的语法约束。与传统 TypeScript 相比,ArkTS 有更多的限制(不支持 any、不支持索引签名、不支持解构赋值等),这可能导致从 TypeScript 迁移的开发者遇到一些"意料之外"的编译错误。
建议:在项目启动前建立 ArkTS 语法检查清单,将常见的"TypeScript 写法 → ArkTS 写法"对照表整理成文档,减少试错成本。
教训2:Mock 数据的阶段化策略
当前版本使用 Mock 数据,但 Service 层的接口设计已经考虑了未来替换为真实 AI 模型的需要。这种"先 Mock 后替换"的策略可以有效降低开发初期的复杂度,但需要注意:
- Mock 数据的字段结构和类型必须与真实数据完全一致
- Service 接口的签名应在设计阶段确定,避免后期修改
- Mock 数据要覆盖各种边界情况,确保 UI 层能正确处理
教训3:状态变量的粒度把控
showResult 和 resultData 两个状态变量分别控制"是否展示"和"展示什么",这种分离设计比使用一个复合状态(如 result: { data: ..., visible: boolean })更清晰,也更易于扩展。
教训4:颜色方案的一致性维护
在开发过程中,需要维护多个颜色值(背景色、文字色、边框色、按钮色等)。如果这些颜色值分散在代码中,后期修改主题时会非常困难。建议将颜色值统一抽取为常量或资源文件($r 引用),而非直接使用字面量。
改进方向
方向1:接入真实 AI 模型
当前版本使用 Mock 数据,后续可接入 HarmonyOS 的端侧 AI 推理能力,实现真正的智能推荐。具体方案包括:
- 使用 MindSpore Lite 在端侧运行轻量级推荐模型
- 接入 HarmonyOS AI Kit 提供的智能推荐服务
- 通过网络请求调用云端 AI 模型 API
方向2:增加输入验证
当前版本对输入内容没有做校验,后续可增加:
- 输入长度限制(防止超长文本)
- 输入格式校验(如特殊字符过滤)
- 必填项提示(如果某个字段必须填写)
方向3:增强交互反馈
点击按钮后可以增加加载动画,提升用户体验:
// 使用 LoadingProgress 组件
if (this.isLoading) {
LoadingProgress().width(32).height(32).color('#5C4033')
}
方向4:数据持久化
使用首选项(Preferences)API 或 @StorageLink,将推荐结果保存到本地,方便用户回顾:
import { preferences } from '@kit.ArkData'
// 保存推荐结果到本地存储
方向5:动画过渡
结果展示区可以使用 animateTo 实现平滑的入场动画,提升视觉体验:
animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
this.showResult = true
})
方向6:国际化支持
当前应用的 UI 文本是硬编码的中文。后续可以接入 HarmonyOS 的国际化资源管理,将文本抽取到 resources 目录下的 element/string.json 文件中,通过 $r('app.string.xxx') 引用,实现多语言支持。
6.4 总结
"AI书单推荐"应用的开发过程,完整地展示了在 HarmonyOS 平台上使用 ArkTS/ArkUI 进行原生应用开发的典型流程。从需求对齐到架构设计,从任务分解到质量审核,从代码实现到复盘评估,每一个阶段都体现了 HarmonyOS 应用开发的独特方法论。
核心发现
通过这个项目,我们验证了以下五个关键结论:
1. ArkTS 是生产就绪的语言
尽管有较多的语法约束(不支持 any、不支持索引签名、不支持解构赋值等),但这些约束在编译时提供了更强大的安全保障,减少了运行时错误。据统计,静态类型检查可以在编译阶段捕获约 70% 的常见编码错误,这在大规模团队协作中尤为重要。
2. ArkUI 声明式 UI 高效直观
@State 驱动的响应式编程模型,让 UI 开发更加简洁和可预测。开发者只需关注"状态与 UI 的映射关系",无需手动操作组件树。这种范式在 UI 复杂度增长时,代码的可维护性优势会更加明显。
3. MVVM 架构在 ArkTS 中完全可行
通过适当的适配,MVVM 的三个层次可以在 ArkTS 的约束下优雅地实现。View 层专注于 UI 描述,Service 层处理业务逻辑,Model 层定义数据结构。三层各司其职,互不干扰。
4. 零外部依赖是可行的
HarmonyOS 的系统 Kit 提供了足够的能力,大多数应用场景不需要引入第三方依赖。这带来的好处包括:更小的包体积、更少的版本兼容性问题、更快的构建速度和更高的应用稳定性。
5. AI 应用的原生化是趋势
将 AI 能力嵌入到原生应用中,而不是依赖 WebView 或小程序,可以提供更好的用户体验和性能。HarmonyOS 的端侧 AI 能力(如 MindSpore Lite、AI Kit)为原生 AI 应用提供了强大的基础设施。
对 HarmonyOS 开发者的建议
基于本次开发实践,我们为 HarmonyOS 开发者提供以下建议:
- 入门路径:从 MVVM 架构入手,先理解 ArkTS 的语法约束,再学习 ArkUI 的声明式 UI 开发范式。
- 工具链:充分利用 DevEco Studio 的代码补全、实时预览和调试功能,这些工具可以显著提升开发效率。
- 设计模式:优先采用 MVVM 架构,将业务逻辑与 UI 展示分离,提高代码的可测试性和可维护性。
- 资源管理:善用 HarmonyOS 的 resources 资源管理机制,将字符串、颜色、图片等资源统一管理。
- 渐进式开发:采用"先 Mock 后替换"的策略,先用 Mock 数据完成 UI 开发和联调,再逐步接入真实 AI 模型或后端服务。
附录:文件路径汇总
- 视图层:
entry/src/main/ets/apps/AI书单推荐/AI书单推荐Page.ets- 数据模型层:
entry/src/main/ets/apps/AI书单推荐/AI书单推荐Model.ets- 业务逻辑层:
entry/src/main/ets/apps/AI书单推荐/AI书单推荐Service.ets- 主应用入口:
entry/src/main/ets/pages/Index.ets- 应用元信息:
entry/src/main/resources/rawfile/apps/apps.json- 项目配置:
build-profile.json5/entry/build-profile.json5- 模块配置:
entry/src/main/module.json5- 依赖配置:
entry/oh-package.json5
本文基于 HarmonyOS API 6.0.1 (21) 和 ArkTS Stage Mode 编写,由 DevEco Studio 构建验证。
更多推荐

所有评论(0)