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数据)
数据持久化 无需持久化,结果仅当次展示
需求理解深化

在深入阅读源代码后,我梳理出以下关键需求点:

  1. 输入采集:用户需要提供三个维度的信息——兴趣(如"科幻小说")、阅读水平(如"中级")、目标(如"拓展视野")。这些信息通过 TextInput 组件采集,以键值对形式存储在 inputData 中。
  2. 智能推荐:点击"整理书架"按钮后,Service 层根据输入生成推荐结果。当前版本使用 Mock 数据作为演示,预留了接入真实 AI 模型的接口。
  3. 结果展示:推荐结果以卡片形式展示在页面下方,包含书籍列表和多维度详情信息,用户可直观查看推荐内容。
  4. 导航交互:页面顶部提供返回按钮,支持从主应用列表页(Index.ets)跳入后返回。

1.3 疑问澄清与决策

在开发过程中,遇到了多个关键决策点,每个决策都需要在 ArkTS 语法约束和实际开发需求之间找到平衡。

决策1:ArkTS 语法约束下的状态管理

疑问:ArkTS 不支持 anyunknown 类型,也不支持索引签名,如何管理动态表单数据?

上下文分析:在书单推荐应用中,用户输入包含三个字段——兴趣、阅读水平和目标。这些字段是动态的,未来可能增加或减少。在传统 TypeScript 中,我们可能会使用 any{[key: string]: string} 索引签名来管理。但 ArkTS 明确禁止了这两种方式。

决策:使用 Record<string, Object> 类型替代 anyRecord 是 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() { /* 结果展示内容 */ }
}

这里有一个关键的设计考量:为什么需要 showResultresultData !== 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),但考虑到:

  1. 展示时最终需要转换为字符串
  2. 统一使用 string 简化了序列化和反序列化
  3. 未来接入 AI 模型后,返回的 JSON 数据天然是字符串格式
export class AI书单推荐Data {
  books: string[] = []
  title: string = ''
  author: string = ''
  category: string = ''
  // ... 所有字段均为 string 类型
}

1.4 最终共识

经过对齐阶段的分析和决策,形成以下共识文档:

需求描述

构建一个 HarmonyOS 原生 AI 书单推荐应用,用户通过输入兴趣、阅读水平和目标,获取个性化的书籍推荐列表和详细的书籍信息。

验收标准
  1. 用户可输入三个维度的文本信息
  2. 点击"整理书架"按钮后生成推荐结果
  3. 推荐结果包含书籍列表和15个维度的详情信息
  4. 页面支持从首页跳入和返回
  5. 界面采用暖色调书架风格,与主应用设计语言一致
  6. 输入框为空时点击按钮应能正常处理
技术方案
  • 语言: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),在 showResulttrueresultData 不为 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
}

设计要点

  1. 方法签名generateData(input: Record<string, Object>): AI书单推荐Data 明确输入输出类型,避免了 any 的使用。输入采用 Record<string, Object> 兼容表单的动态字段,输出返回强类型的 AI书单推荐Data 对象。

  2. 防御性编程String(input['interest'] || '') 确保即使输入字段不存在或为空,也不会导致运行时错误。|| 运算符在左侧为 undefinednull 或空字符串时都会回退到空字符串,再通过 String() 确保结果是 string 类型。

  3. 扩展性预留:Mock 数据生成逻辑可以无缝替换为调用 AI 模型 API 的真实逻辑,只需修改 generateData 方法内部实现,接口不变,View 层无需任何改动。这种"面向接口编程"的设计模式是 MVVM 架构的核心优势。

  4. 私有成员 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
  └── 无外部依赖(纯数据类)

这种依赖关系确保了:

  1. Model 层完全独立AI书单推荐Data 类不依赖任何其他模块,可被任意模块引用,甚至可以独立抽取为公共库。
  2. Service 层只依赖 Model:Service 层不依赖任何 UI 相关的模块,这使得 Service 层可以在纯逻辑环境中进行单元测试,无需渲染引擎支持。
  3. 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 层执行以下操作:

  1. inputData 中提取 interest 字段
  2. 执行数据生成逻辑(当前为 Mock 数据,未来可替换为 AI 模型推理)
  3. 构造并返回 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) 命中,结果卡片展示在按钮下方,用户看到完整的推荐结果。

数据流的核心特性

  1. 可追踪性:数据流向是单向的,任何时候都可以清楚地知道数据从哪里来、到哪里去,大大降低了调试难度。
  2. 可预测性:给定相同的输入,数据流总是产生相同的输出,不存在副作用。
  3. 高效性@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 ✅ 合规 使用 ForEachfor 循环
不支持 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 = ''
  }
}

设计要点

  1. 字段默认值:所有字段在声明时赋予默认值,这符合 ArkTS 的要求——不支持确定性赋值断言(let v!: T),必须使用带初始化的声明。
  2. 构造函数显式初始化:虽然在声明时已赋值,但在构造函数中再次显式初始化,确保实例化时的确定性。这在 ArkTS 的严格模式下是推荐做法。
  3. 无外部依赖: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
  }
}

设计要点

  1. 类型安全:方法签名 generateData(input: Record<string, Object>): AI书单推荐Data 明确输入输出类型,避免了 any 的使用。
  2. 防御性编程String(input['interest'] || '') 确保即使输入字段不存在或为空,也不会导致运行时错误。
  3. 扩展性预留: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.servicethis.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"
    }]
  }
}

关键参数说明:

  • targetSdkVersioncompatibleSdkVersion 都设为 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 会自动完成以下步骤:

  1. 资源编译:编译 resources 目录下的资源文件(字符串、颜色、图片等)
  2. ArkTS 源码编译:将 .ets 文件编译为方舟字节码
  3. 链接与打包:将所有编译产物链接为 .hap 包
  4. 签名与部署:使用配置的签名文件对包进行签名,部署到模拟器或真机
功能测试矩阵
测试用例 输入 预期结果 实际结果
正常输入 兴趣=科幻,阅读水平=中级,目标=拓展视野 展示推荐结果 ✅ 通过
仅输入兴趣 兴趣=科幻,其余为空 展示推荐结果(含空字段) ✅ 通过
全部为空 三个输入框均为空 展示推荐结果(默认值) ✅ 通过
超长输入 输入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 的静态类型方言,在开发中带来了显著的优势,同时也伴随着一些需要适应的挑战。

优势分析

  1. 编译时错误捕获:许多在 JavaScript 中需要运行时才能发现的类型错误,在 ArkTS 中编译阶段就能被捕获。例如,如果误将 string 类型的值赋值给 number 类型的变量,编译会立即报错,而不是等到运行时才暴露问题。

  2. 更好的 IDE 支持:明确的类型声明让 DevEco Studio 的代码补全和重构功能更加精准。在输入 this.resultData. 后,IDE 能精确列出所有可用字段,极大提升了编码效率。

  3. 性能优化:静态类型允许方舟编译器生成更高效的机器码,减少运行时类型检查开销。对于移动端应用来说,这意味着更快的启动速度和更流畅的交互体验。

  4. 代码可读性:类型声明本身就是一种文档。阅读 generateData(input: Record<string, Object>): AI书单推荐Data 这个签名,不需要看实现就能理解方法的功能——接收表单数据,返回推荐结果。

挑战与应对

  1. 不支持 any/unknown:在需要处理动态数据时,需要借助 Record<string, Object> 等工具类型,并配合显式类型转换。这增加了少量样板代码,但换来了更强的类型安全性。

  2. 不支持索引签名:不能使用 obj["field"] 语法,需要统一使用 obj.field。对于动态键名的场景,Record<string, Object> 是标准解决方案。

  3. 不支持解构赋值:在 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 数据结构定义 纯数据类,无行为

这种模式的优势

  1. View 和 Service 完全解耦:Service 层不依赖任何 UI 类型,可以在纯逻辑环境中独立测试。如果需要,可以编写纯 ArkTS 的单元测试来验证 Service 层的逻辑正确性。

  2. 数据流清晰,状态变更可追踪:所有数据变更都通过 @State 变量进行,数据流向是单向的:用户操作 → Service 处理 → 状态更新 → UI 渲染。这种单向数据流大大降低了调试难度。

  3. 业务逻辑可复用:同一 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:状态变量的粒度把控

showResultresultData 两个状态变量分别控制"是否展示"和"展示什么",这种分离设计比使用一个复合状态(如 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 开发者提供以下建议:

  1. 入门路径:从 MVVM 架构入手,先理解 ArkTS 的语法约束,再学习 ArkUI 的声明式 UI 开发范式。
  2. 工具链:充分利用 DevEco Studio 的代码补全、实时预览和调试功能,这些工具可以显著提升开发效率。
  3. 设计模式:优先采用 MVVM 架构,将业务逻辑与 UI 展示分离,提高代码的可测试性和可维护性。
  4. 资源管理:善用 HarmonyOS 的 resources 资源管理机制,将字符串、颜色、图片等资源统一管理。
  5. 渐进式开发:采用"先 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 构建验证。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐