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

1. 引言:鸿蒙原生开发的范式之变

HarmonyOS NEXT 是华为全栈自研的操作系统,从内核到框架彻底去除了 AOSP 代码。对于应用开发者来说,最直观的变化是:不再支持 Android 应用兼容,必须使用鸿蒙原生语言开发。

ArkTS 是鸿蒙原生应用的首选开发语言,它基于 TypeScript 语法扩展,继承了 TypeScript 的类型安全特性,同时增加了 ArkUI 声明式 UI 框架所需的 @Component@State@Prop 等装饰器语法。

这套技术栈的核心思路可以概括为:

ArkTS(语言层)→ 声明式 UI 描述 → ArkUI 运行时(渲染层)→ 鸿蒙内核

本文将以一个完整的信息流应用为例,从最简单的 Column 布局开始,逐步深入到 Scroll 滚动容器、组件化架构、响应式数据管理和算法实现,帮助读者系统掌握鸿蒙 NEXT 应用开发的核心技能。


2. 项目全景:从结构到运行

先来看项目目录结构,理解一个鸿蒙 NEXT 应用是如何组织的:

MyApplication/
├── AppScope/                  # 应用全局配置
│   ├── app.json5              # 应用级配置(bundleName、版本号等)
│   └── resources/             # 全局资源
├── entry/                     # 主模块(entry 类型)
│   ├── src/main/
│   │   ├── ets/               # ArkTS 源码目录
│   │   │   ├── components/    # 可复用组件
│   │   │   ├── entryability/  # Ability(页面生命周期)
│   │   │   ├── model/         # 数据模型与算法
│   │   │   └── pages/         # 页面
│   │   ├── module.json5       # 模块配置
│   │   └── resources/         # 模块级资源
│   ├── build-profile.json5    # 构建配置
│   └── oh-package.json5       # 依赖管理
└── build-profile.json5        # 项目级构建配置

核心配置文件解读:

build-profile.json5 确定了 SDK 版本:

{
  "app": {
    "products": [
      {
        "name": "default",
        "targetSdkVersion": "6.1.1(24)",
        "compatibleSdkVersion": "6.1.1(24)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  }
}

这里 targetSdkVersioncompatibleSdkVersion 均为 6.1.1(24),即 API 24,对应 HarmonyOS NEXT 6.1.1。

EntryAbility.ets 是应用的入口,通过 windowStage.loadContent 加载初始页面:

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/ColumnScrollPage', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'EnglishApp', 'Failed to load content: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'EnglishApp', 'Succeeded in loading content.');
    });
  }
}

每个 Ability 对应一个应用入口,loadContent 接受页面路径(基于 src/main/ets/pages/ 目录相对路径)。这里的 pages/ColumnScrollPage 对应 pages/ColumnScrollPage.ets 文件。


3. ArkTS 语言速览:声明式的力量

ArkTS 本质上是一种装饰器驱动的声明式 UI 语言。与传统的命令式编程不同,ArkTS 开发者只需描述 UI 应该是什么样子,框架自动负责渲染和更新。

3.1 核心装饰器

装饰器 作用 类比
@Component 标记一个结构体为可复用的 UI 组件 React 的 class component
@Entry 标记页面入口 Vue 的页面级组件
@State 声明响应式状态变量(可变) React 的 useState
@Prop 声明从父组件传入的属性(不可变) React 的 props
@BuilderParam 注入自定义构建内容 Vue 的 slot 插槽

3.2 声明式语法的基本模式

@Component
struct MyComponent {
  @State private count: number = 0;

  build() {
    Column() {
      Text(`计数:${this.count}`)
        .fontSize(16)

      Button('增加')
        .onClick(() => {
          this.count++;  // 状态变化 → 自动重渲染
        })
    }
    .width('100%')
    .padding(12)
  }
}

关键点:

  • build() 方法是组件的渲染入口
  • 链式调用 .attribute(value) 设置样式属性
  • @State 变量变化时,框架自动重新执行 build() 更新 UI
  • 不需要手动操作 DOM,也不需要维护 View 层状态同步

3.3 与标准 TypeScript 的差异

ArkTS 对 TypeScript 做了严格的限制以保证运行时性能:

  • 不支持 any 类型:所有变量必须有明确的类型
  • 不支持 JavaScript 动态特性:如 evalwith、原型链操作
  • 严格空安全:变量默认不可为 null/undefined,需要显式声明
  • 有限制的表达式:部分复杂表达式需要在 build() 之外计算

这些限制初看让人不习惯,但实际开发中能避免大量运行时错误,并且让框架的编译时优化成为可能。


4. 布局基石:Column 容器的深度拆解

在 ArkUI 中,布局容器是 UI 构建的核心。Column 是最基础的容器之一——它将子组件沿**垂直方向(主轴)**依次排列。

4.1 Column 的基本使用

Column() {
  Text('第一项').fontSize(16)
  Text('第二项').fontSize(16)
  Text('第三项').fontSize(16)
}
.width('100%')

以上代码将三个文本垂直排列。Column 的特点:

特性 说明
主轴(Main Axis) 垂直方向(从上到下)
交叉轴(Cross Axis) 水平方向(从左到右)
主轴对齐控制 justifyContent()
交叉轴对齐控制 alignItems()

4.2 Column 与 Row 的对比

ColumnRow 是 ArkUI 中最基础的两个布局容器,可以理解为 Flexbox 的两个方向特化:

Column = flex-direction: column
Row    = flex-direction: row
容器 主轴方向 主轴控制 交叉轴控制
Column 垂直 ↓ justifyContent alignItems(HorizontalAlign)
Row 水平 → justifyContent alignItems(VerticalAlign)

4.3 实战:信息卡片组件的 Column 布布局

在我们的项目中,InfoCard 组件内部使用了 Column 来组织标题和描述:

@Component
struct InfoCard {
  private item: InfoItem = { title: '', desc: '' };
  private index: number = 0;

  build() {
    Column() {
      // 标题
      Text(this.item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1a1a2e')
        .lineHeight(22)

      // 描述文字
      Text(this.item.desc)
        .fontSize(13)
        .fontColor('#666666')
        .lineHeight(20)
        .margin({ top: 6 })
    }
    .alignItems(HorizontalAlign.Start)   // 卡片内文字左对齐
    .width('100%')
    .padding(14)
    .backgroundColor('#f8f9fc')
    .borderRadius(10)
    .shadow({ radius: 4, color: '#20000000', offsetX: 0, offsetY: 2 })
    .margin({ bottom: 10 })
  }
}

这里展示了几点:

  1. alignItems(HorizontalAlign.Start):让卡片内的所有子组件水平左对齐,这是最常见的文字列表排版方式。
  2. 链式调用的顺序:虽然链式调用可以任意排列,但推荐按"布局→视觉→交互"的顺序组织,提高可读性。
  3. 阴影的写法shadow 属性接受一个对象,包含 radius(模糊半径)、color(颜色,含 alpha)、offsetX/offsetY(偏移量)。

4.4 layoutWeight:弹性权重的妙用

在 Column 或 Row 中,layoutWeight 属性可以让子组件按比例分配剩余空间:

Column() {
  Text('固定高度内容')
    .height(40)

  Text('占满剩余空间')
    .layoutWeight(1)   // 类似 flex: 1
    .width('100%')
}
.width('100%')
.height(200)

这与 CSS Flexbox 中的 flex: 1 概念一致。在页面布局中,这是实现"固定头尾 + 中间撑满"的核心手段。


5. 主轴对齐:justifyContent 的精妙控制

justifyContent 控制子组件在主轴方向的排列方式。对于 Column 来说,就是垂直方向上的排列。

5.1 FlexAlign 枚举全览

enum FlexAlign {
  Start,        // 顶部起始排列(默认值)
  Center,       // 垂直居中排列
  End,          // 底部排列
  SpaceBetween, // 均匀分布,首尾贴边
  SpaceAround,  // 均匀分布,两侧间距为中间的一半
  SpaceEvenly,  // 完全均匀分布,所有间距相等
}

5.2 实战:justifyContent(FlexAlign.Start)

Index.ets 的核心演示区中:

Column() {
  // ... 大量子组件
}
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.Start)    // ★ 主轴顶部起始
.width('100%')
.height(0)
.layoutWeight(1)
.padding(16)

这里设置 height(0) + layoutWeight(1) 的组合值得注意:

  • height(0) 先设置一个初始高度
  • layoutWeight(1) 告诉父容器:本 Column 占满所有剩余空间

当 Column 的高度被撑满后,justifyContent(FlexAlign.Start) 的效果就是:所有子组件紧凑排列在容器顶部,底部留空

这与 FlexAlign.Center 的区别十分直观:

FlexAlign.Start:  [A][B][C]_________
FlexAlign.Center: ______[A][B][C]______
FlexAlign.End:    _________[A][B][C]

5.3 justifyContent 的调试思路

在鸿蒙开发中,布局不符合预期时,优先排查:

  1. 父容器是否有明确的高度/宽度? 没有明确尺寸时,justifyContent 可能不起作用。
  2. layoutWeight 是否正确设置? 在 Column 中,子组件需要撑满高度时,设置 layoutWeight(1)
  3. 是否有 margin 或 padding 干扰? margin 在 justifyContent 计算之外。

一个实用的调试技巧:给容器设置鲜艳的背景色,观察子组件实际的分布区域。


6. 交叉轴对齐:alignItems 的配合艺术

如果说 justifyContent 控制主轴方向(纵轴),那么 alignItems 就控制交叉轴方向(横轴)。在 Column 中,交叉轴是水平方向。

6.1 HorizontalAlign 枚举

enum HorizontalAlign {
  Start,  // 左对齐
  Center, // 水平居中(默认值)
  End,    // 右对齐
}

注意 Column 的默认 alignItemsHorizontalAlign.Center,这与 CSS Flexbox 的 align-items: stretch 不同——ArkUI 的 Column 默认不会拉伸子组件的宽度。

6.2 对齐方式的组合效果

通过 justifyContent + alignItems 的不同组合,可以快速实现 9 种布局定位:

justifyContent alignItems(Start) alignItems(Center) alignItems(End)
Start ⬆⬅ 左上 ⬆ 上居中 ⬆➡ 右上
Center 中左 正中 中右
End ⬇⬅ 左下 ⬇ 下居中 ⬇➡ 右下

6.3 父子组件对齐的传递与覆盖

父容器的 alignItems 会作用于所有直接子组件,但子组件可以通过自身的属性来调整:

Column() {
  Text('左对齐')
    .align(Alignment.Start)  // 自定义对齐方式

  Text('居中')
    // 继承父容器的 alignItems
}
.alignItems(HorizontalAlign.Start)

align() 用法更灵活,可以精确控制单个组件在父容器中的对齐位置。


7. Scroll 滚动容器:突破屏幕边界

当内容超出屏幕尺寸时,滚动容器就是必需品。在 ArkUI 中,Scroll 组件提供了这个能力。

7.1 Scroll + Column 的核心模式

这是鸿蒙开发中最常见的滚动列表模式:

Scroll(scroller) {
  Column({ space: 12 }) {
    // 大量子组件...
    ForEach(dataList, (item: Item) => {
      ItemCard({ item: item })
    }, (item: Item) => item.id.toString())
  }
  .width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Auto)

核心原理:

┌─────────────────────────┐
│  外层 Column(全屏)    │
│  ┌───────────────────┐  │
│  │ 固定标题区         │  │  ← 固定高度,不参与滚动
│  ├───────────────────┤  │
│  │ Scroll 容器       │  │  ← layoutWeight(1) 撑满剩余
│  │ ┌───────────────┐ │  │
│  │ │ Column(内容)   │ │  │  ← 高度由内容撑开
│  │ │ 卡片1          │ │  │
│  │ │ 卡片2          │ │  │
│  │ │ 卡片3          │ │  │
│  │ │ ...            │ │  │
│  │ └───────────────┘ │  │
│  ├───────────────────┤  │
│  │ 固定底部状态栏     │  │  ← 固定高度,不参与滚动
│  └───────────────────┘  │
└─────────────────────────┘

关键洞察:Scroll 不关心内部 Column 有多少内容,它只负责两件事——① 检测内容高度是否超过自身容器高度;② 如果超过,启用滚动交互。

7.2 ForEach 的高性能渲染

ForEach 是 ArkUI 中的列表渲染函数,对应 React 的 map() 或 Vue 的 v-for

ForEach(
  array: any[],                          // 数据源
  (item: any, index?: number) => void,   // 内容生成器
  key?: (item: any, index?: number) => string  // 键值生成器
)

第三个参数 key 函数非常重要:它为每个列表项提供唯一标识,让框架在列表更新时可以复用已有组件,避免全量销毁重建。

ForEach(
  this.getDisplayData(),
  (item: ListEntry) => {
    ListItemCard({ item: item })
  },
  (item: ListEntry) => item.id.toString()  // 以 id 作为键
)

id 不变时,即使列表重新渲染,已有 ListItemCard 组件也不会重新创建,性能大大提升。

7.3 实战:江湖大事记的 50 条动态数据

我们的 ColumnScrollPage 包含 50 条精心编写的江湖内容数据,每条内容长度不一,自然地产生了不同高度的卡片:

private readonly fullData: ListEntry[] = [
  {
    id: 1,
    category: '江湖轶事',
    title: '无名剑客三招败尽江南七怪',
    content: '昨夜西湖畔,一名无名剑客仅凭三招剑法便连败江南七怪...',
    author: '百晓生',
    likes: 342,
    time: '巳时三刻',
    tag: '热门'
  },
  // ... 49 条更多内容
];

每条 content 的长度从 50 到 200 字不等,对应渲染出的卡片高度也不同,让滚动效果更自然真实。


8. 三段式页面架构:固定头 + 滚动体 + 固定底

这是移动端最经典的页面架构之一。其核心在于:外层 Column 不可滚动,仅作为布局容器;内部的 Scroll 组件单独管理滚动行为。

8.1 架构实现

Column() {
  // ── 段 1:顶部标题区(固定高度,不滚动)──
  Column() {
    Text('📜 江湖大事记')
      .fontSize(20).fontWeight(FontWeight.Bold)
      .fontColor('#ffffff')

    Text('Column + Scroll 滚动纵向布局 · 内容超屏自动滚动')
      .fontSize(12).fontColor('#cce0ff')
      .margin({ top: 4 })
  }
  .alignItems(HorizontalAlign.Start)
  .width('100%')
  .padding({ top: 20, bottom: 12, left: 20, right: 20 })
  .backgroundColor('#2d5f8a')


  // ── 段 2:核心 — Scroll 接管滚动 ──
  Scroll(this.scroller) {
    Column({ space: 12 }) {
      ForEach(this.getDisplayData(), (item: ListEntry) => {
        ListItemCard({ item: item })
      }, (item: ListEntry) => item.id.toString())
    }
    .width('100%')
    .padding({ left: 12, right: 12, top: 10, bottom: 10 })
  }
  .layoutWeight(1)              // ★ 占满剩余空间
  .width('100%')
  .scrollBar(this.currentBarState)
  .edgeEffect(EdgeEffect.Spring)


  // ── 段 3:底部状态条(固定高度,不滚动)──
  Row() {
    // 状态信息...
  }
  .width('100%')
  .height(36)
  .backgroundColor('#ffffff')
}
.width('100%')
.height('100%')

8.2 为什么外层 Column 不能滚动?

这是一个容易犯的错误:如果把滚动的能力放在外层 Column 上,那么整个页面都会滚动,包括标题栏和底部状态栏——这会导致标题栏滚动出屏幕,底部状态栏消失在视口之外,这是不符合移动端设计规范的。

正确做法是:只让内容区滚动,头部和底部始终固定。

8.3 layoutWeight 在分段布局中的作用

layoutWeight 在分段布局中的作用类似于 Android 的 layout_weight 或 CSS Flexbox 的 flex: 1

  • 标题区:固定高度(由内部内容撑开)
  • 控制面板:固定高度(由内部内容撑开)
  • Scroll 区:layoutWeight(1) → 吃掉父容器去掉头部和底部后的所有剩余空间
  • 底部状态栏:固定高度(height(36)

这种布局方式确保了 Scroll 区总能得到精确的可用高度,不多不少,滚动行为自然正确。


9. 编程式滚动:Scroller 控制器的妙用

Scroller 是 ArkUI 中控制 Scroll 容器的编程接口,可以实现编程式滚动、获取滚动状态等高级功能。

9.1 Scroller 的基本用法

// 1. 创建 Scroller 实例
private scroller: Scroller = new Scroller();

// 2. 绑定到 Scroll
Scroll(this.scroller) {
  // ...
}

// 3. 编程控制
this.scroller.scrollTo({
  xOffset: 0,
  yOffset: 0,
  animation: { duration: 500 }  // 动画时长 500ms
});

9.2 滚动到指定位置

// 滚动到顶部(带动画)
this.scroller.scrollTo({
  xOffset: 0,
  yOffset: 0,
  animation: { duration: 500 }
});

// 滚到底部(使用 scrollEdge)
this.scroller.scrollEdge(Edge.Bottom);

scrollToscrollEdge 的区别:

方法 适用场景 特点
scrollTo({xOffset, yOffset, animation}) 精确位置 指定具体偏移量,可带动画
scrollEdge(Edge.Top/Bottom) 到顶/到底 直接滚动到边缘,精确快速

9.3 滚动事件监听

Scroll(this.scroller) {
  // ...
}
.onScroll((xOffset: number, yOffset: number) => {
  // ★ 实时获取滚动偏移量
  this.currentOffset = `${Math.round(yOffset)}vp`;
})
.onScrollStop(() => {
  hilog.info(0x0000, TAG, 'Scroll stopped at: %s', this.currentOffset);
})
.onScrollEdge((side: Edge) => {
  if (side === Edge.Top) {
    hilog.info(0x0000, TAG, 'Reached top');
  } else if (side === Edge.Bottom) {
    hilog.info(0x0000, TAG, 'Reached bottom');
  }
})

三个事件的配合十分实用:

  1. onScroll:实时反馈,适合更新 UI(如位置指示器、回到顶部按钮的显隐)
  2. onScrollStop:滚动停止后触发,适合记录日志、触发数据加载
  3. onScrollEdge:触边事件,适合实现"加载更多"(到底部触发)

9.4 实战:滚动控制面板

我们的应用包含一个完整的滚动控制面板,提供:

  • 实时滚动位置显示:偏移:1256vp
  • 一键回到顶部 / 滚到底部
  • 动态增加列表项(每次 +5 条)
  • 重置列表(回到 10 条)
  • 滚动条模式切换(Auto / On / Off)
// 回到顶部
Button('⬆ 回到顶部')
  .onClick(() => {
    this.scroller.scrollTo({
      xOffset: 0,
      yOffset: 0,
      animation: { duration: 500 }
    });
  })

// 滚到底部
Button('⬇ 滚到底部')
  .onClick(() => {
    this.scroller.scrollEdge(Edge.Bottom);
  })

这个面板不仅方便开发和调试,也是向用户展示"我可以编程控制滚动"的绝佳交互示例。


10. 滚动条状态管理:Auto / On / Off

scrollBar() 方法控制滚动条的显示行为,接受 BarState 枚举值:

enum BarState {
  Auto,  // 滚动时显示,停止后自动隐藏(默认)
  On,    // 始终显示
  Off,   // 始终隐藏
}

10.1 状态管理与交互反馈

在我们的应用中,滚动条状态通过 @State 变量管理,并提供可视化切换:

@State private currentBarState: BarState = BarState.Auto;

// 切换状态
private setBarState(state: BarState): void {
  this.currentBarState = state;
  const labels: string[] = ['自动显示', '常时显示', '始终隐藏'];
  const idx = [BarState.Auto, BarState.On, BarState.Off].indexOf(state);
  promptAction.showToast({
    message: `滚动条模式:${labels[idx]}`,
    duration: 1500
  });
}

UI 上的切换按钮使用条件样式来指示当前激活状态:

Text('Auto')
  .fontColor(this.currentBarState === BarState.Auto ? '#fff' : '#666')
  .backgroundColor(this.currentBarState === BarState.Auto ? '#3a7bd5' : '#f0f0f0')
  .onClick(() => { this.setBarState(BarState.Auto); })

10.2 滚动条宽度定制

.scrollBarWidth(6)  // 自定义滚动条宽度为 6vp

鸿蒙对滚动条的定制能力相当灵活,包括宽度、颜色、圆角等都可以在主题中统一配置。

10.3 边界回弹效果

.edgeEffect(EdgeEffect.Spring)  // 弹簧回弹效果

EdgeEffect 提供了两种选择:

  • Spring:像 iOS 一样的弹簧回弹效果,滚动到边界时出现弹性阻尼
  • None:硬边界,到顶/到底立即停止

Spring 效果更接近用户直觉,建议在绝大多数场景中使用。


11. 组件化设计:从 @Component 到 @BuilderParam

组件化是大型应用开发的基石。ArkTS 提供了完整的组件化机制,从基础 @Component 到高级的 @BuilderParam 插槽模式。

11.1 组件的层级结构

在我们的项目中,组件层级可以清晰地梳理为:

App (EntryAbility)
 └── ColumnScrollPage (@Entry @Component)
      ├── AppHeader (可复用顶部标题栏)
      ├── ScrollControlBar (滚动控制面板)
      │    └── 内置 Button × 4
      ├── Scroll
      │    └── Column
      │         └── ListItemCard × N (每一条江湖消息)
      └── 底部状态栏 (内联)

11.2 子组件:ListItemCard

ListItemCard 是一个典型的数据驱动子组件,通过 @Prop 接收数据并渲染:

@Component
struct ListItemCard {
  private item: ListEntry = {
    id: 0, category: '', title: '', content: '',
    author: '', likes: 0, time: '', tag: ''
  };

  build() {
    Column() {
      // ① 分类 + 标签(Row 横向分布)
      Row() {
        Text(this.item.category)
          .fontSize(10)
          .fontColor('#ffffff')
          .backgroundColor(this.getCategoryColor(this.item.category))
          .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          .borderRadius(4)

        if (this.item.tag) {
          Text(this.item.tag)
            .fontSize(10)
            .fontColor('#ff9800')
            .border({ width: 1, color: '#ff9800' })
            .borderRadius(4)
            .margin({ left: 6 })
        }

        Text(this.item.time)
          .layoutWeight(1)    // 占满剩余,实现右对齐
          .textAlign(TextAlign.End)
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')

      // ② 标题
      Text(this.item.title)
        .fontSize(16).fontWeight(FontWeight.Bold)
        .fontColor('#1a1a2e').lineHeight(24)
        .margin({ top: 8 })

      // ③ 正文(长度不一 → 卡片高度不同)
      Text(this.item.content)
        .fontSize(13).fontColor('#555555')
        .lineHeight(22).margin({ top: 6 })

      // ④ 底部信息栏
      Row() {
        Row() {
          Text('✍').fontSize(12)
          Text(this.item.author)
            .fontSize(12).fontColor('#888888')
            .margin({ left: 4 })
        }
        .layoutWeight(1)

        Row() {
          Text('👍').fontSize(12)
          Text(`${this.item.likes}`)
            .fontSize(12).fontColor('#888888')
            .margin({ left: 4 })
        }
        .margin({ right: 16 })

        Text('阅读全文 ›')
          .fontSize(12).fontColor('#3a7bd5')
          .onClick(() => {
            promptAction.showToast({
              message: `打开「${this.item.title}`,
              duration: 1500
            });
          })
      }
      .margin({ top: 10 })
    }
    .alignItems(HorizontalAlign.Start)
    .width('100%')
    .padding(14)
    .backgroundColor('#ffffff')
    .borderRadius(10)
    .shadow({ radius: 4, color: '#18000000', offsetX: 0, offsetY: 2 })
  }
}

组件设计要点:

  1. 约定优于配置:属性名避开与 CommonAttribute 重名的关键字(如 backgroundColorborderRadius),改用带业务含义的名称
  2. 默认值兜底ListItemEntry 的成员都给了空值,避免数据缺失时的渲染崩溃
  3. 内联方法封装getCategoryColor() 将分类到颜色的映射逻辑封装在组件内部,调用者无需关心
  4. 响应式交互:使用 onClick 配合 promptAction.showToast 提供即时反馈

11.3 通用组件:Card 容器

通用卡片容器使用 @BuilderParam 实现类似于 Vue slot 或 React children 的内容注入:

@Component
export struct Card {
  @Prop cardPadding: number = 16;
  @Prop cardMargin: number = 12;
  @Prop cardColor: string = '#ffffff';
  @Prop cardRadius: number = 16;
  @BuilderParam content: () => void = this.defaultContent;

  @Builder
  defaultContent(): void {
    Text('卡片内容')
      .fontSize(14)
      .fontColor('#888')
  }

  build() {
    Column() {
      this.content()  // ★ 调用注入的内容
    }
    .width('100%')
    .padding(this.cardPadding)
    .backgroundColor(this.cardColor)
    .borderRadius(this.cardRadius)
    .margin({ bottom: this.cardMargin })
    .shadow({ radius: 4, color: '#1a000000', offsetX: 0, offsetY: 2 })
  }
}

使用方式:

Card({
  cardPadding: 12,
  cardColor: '#f8f9fc',
  cardRadius: 10
}) {
  // ★ 这里的内容会替换 this.content()
  Text('自定义卡片内容')
  Button('点击操作')
}

@BuilderParam 是 ArkTS 实现"插槽"模式的官方方式,它比通过 @Prop 传递整个组件实例更为灵活。

11.4 通用组件:AppHeader

应用通用的顶部标题栏,支持显示返回按钮:

@Component
export struct AppHeader {
  @Prop headerTitle: string = '';
  @Prop headerSubtitle: string = '';
  @Prop showBack: boolean = false;
  onBack: () => void = () => {};

  build() {
    Row() {
      if (this.showBack) {
        Text('←')
          .fontSize(22).fontColor('#ffffff')
          .onClick(() => { this.onBack(); })
          .margin({ right: 8 })
      }
      Column() {
        Text(this.headerTitle)
          .fontSize(20).fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
        if (this.headerSubtitle.length > 0) {
          Text(this.headerSubtitle)
            .fontSize(12).fontColor('#cce0ff')
            .margin({ top: 2 })
        }
      }
      .alignItems(HorizontalAlign.Start)
      Blank()  // 占满剩余
    }
    .width('100%')
    .padding({ top: 12, bottom: 12, left: 20, right: 20 })
    .backgroundColor('#2d5f8a')
  }
}

Blank() 是一个空心占位组件,在 Row 中会自动撑满剩余空间,实现"文字左对齐"的效果——这比在 CSS 中使用 justify-content: space-between 更简洁。

11.5 通用组件:ModuleEntryCard

模块快捷入口卡片,用于首页的功能导航矩阵:

@Component
export struct ModuleEntryCard {
  @Prop entryIcon: string = '';
  @Prop entryLabel: string = '';
  @Prop entryColor: string = '#3a7bd5';
  onClickAction: () => void = () => {};

  build() {
    Column() {
      Text(this.entryIcon).fontSize(32).margin({ bottom: 8 })
      Text(this.entryLabel)
        .fontSize(13).fontColor('#333')
        .fontWeight(FontWeight.Medium)
    }
    .width('30%')           // 每行放 3 个
    .aspectRatio(1.0)       // 正方形
    .justifyContent(FlexAlign.Center)
    .backgroundColor((this.entryColor + '18'))  // 18 = 10% 透明度
    .borderRadius(16)
    .onClick(() => { this.onClickAction(); })
    .shadow({ radius: 2, color: '#10000000', offsetX: 0, offsetY: 1 })
  }
}

aspectRatio(1.0) 是鸿蒙布局中非常实用的属性——设置宽高比后,组件会根据宽度自动计算高度,无需手动指定。这里实现的正方形入口卡片,无论屏幕宽度如何变化,始终是 1:1 的正方形。


12. 数据模型设计:英语学习 App 的数据层

除了江湖信息流页面,项目中还包含一个完整的英语学习应用的数据模型层,涵盖单词库、听力、阅读、语法、口语五大模块。

12.1 核心数据结构

/** 单词条目 */
export interface WordItem {
  id: number;
  word: string;              // 英文单词
  phonetic: string;          // 音标
  translation: string;       // 中文释义
  partOfSpeech: string;      // 词性
  exampleSentence: string;   // 例句
  exampleTranslation: string;// 例句翻译
  difficulty: Difficulty;    // 难度
  category: string;          // 分类
  audioPath: string;         // 音频路径
}

/** 学习记录 */
export interface StudyRecord {
  wordId: number;
  reviewCount: number;       // 复习次数
  correctCount: number;      // 正确次数
  lastReviewTime: string;    // 上次复习时间
  masteryLevel: number;      // 掌握度 0.0-1.0
}

设计亮点:

  • StudyRecordWordItem 通过 wordId 关联,实现"数据"与"进度"的分离
  • masteryLevel 使用 0.0-1.0 的浮点数,比离散的等级(熟悉/不熟悉)更精细
  • difficulty 使用枚举而非字符串,类型安全且可排序

12.2 难度与词性枚举

export enum Difficulty {
  EASY = 1,
  MEDIUM = 2,
  HARD = 3,
}

export enum PartOfSpeech {
  NOUN = 'n.',
  VERB = 'v.',
  ADJ = 'adj.',
  ADV = 'adv.',
  PREP = 'prep.',
  CONJ = 'conj.',
  PRON = 'pron.',
  INTERJ = 'interj.',
}

使用枚举而非字符串常量有两大好处:编译时类型检查和 IDE 自动补全。

12.3 样例数据:30 个核心单词

export const SAMPLE_WORDS: WordItem[] = [
  {
    id: 1,
    word: 'abandon',
    phonetic: '/əˈbændən/',
    translation: '放弃;遗弃',
    partOfSpeech: 'v.',
    exampleSentence: 'They had to abandon the project due to lack of funds.',
    exampleTranslation: '由于缺乏资金,他们不得不放弃这个项目。',
    difficulty: Difficulty.MEDIUM,
    category: '基础词汇',
    audioPath: ''
  },
  // ... 29 个更多单词
];

数据的组织按难度分层:

难度 数量 代表词汇
EASY(基础) ~12 个 benefit, challenge, journey, knowledge
MEDIUM(核心) ~10 个 determine, maintain, negotiate, adapt
HARD(进阶) ~8 个 elaborate, hypothesis, phenomenon, sustainable

这种分层设计方便后续的"困难优先"复习策略——系统可以自动安排更多时间给高难度词汇。

12.4 阅读与语法模块

阅读文章包含完整的内容和配套问题:

export interface ReadingArticle {
  id: number;
  title: string;
  level: string;           // 初级 / 中级 / 高级
  wordCount: number;
  content: string;         // 文章正文
  questions: ReadingQuestion[];
}

export interface ReadingQuestion {
  id: number;
  type: 'main_idea' | 'detail' | 'inference' | 'vocabulary';
  question: string;
  options: string[];
  correctIndex: number;
}

type 字段使用联合类型(Union Type),限定了四种问题类型,保证类型安全的同时,也让 UI 可以根据问题类型展示不同的交互样式。

语法练习题使用 explanation 字段提供详细的解析:

{
  id: 1,
  topic: '时态 - 现在完成时',
  question: 'She ___ in London for five years.',
  options: ['live', 'lives', 'has lived', 'is living'],
  correctIndex: 2,
  explanation: '现在完成时表示从过去持续到现在的动作:has/have + 过去分词。'
}

这种"题目 + 选项 + 答案 + 解析"的四要素结构,是教育类应用数据设计的标准模式。


13. 间隔重复引擎:SM-2 算法的鸿蒙实现

间隔重复(Spaced Repetition)是语言学习类应用的灵魂。我们基于 SuperMemo 的 SM-2 算法实现了复习调度引擎。

13.1 算法的核心参数

export interface ReviewResult {
  nextInterval: number;    // 下次复习间隔(天)
  newRepetition: number;   // 更新后的连续正确次数
  newEf: number;           // 更新后的易度系数
  nextReview: Date;        // 下次复习日期
}

三个核心参数构成一个状态三元组 (interval, repetition, ef)

  • interval:距离下次复习的天数,决定何时再次见到这个单词
  • repetition:连续答对次数,决定当前学习阶段的进度
  • ef (Easiness Factor):易度系数,反映单词对用户的难易程度

13.2 调度逻辑

export class SpacedRepetitionEngine {
  private static readonly DEFAULT_EF = 2.5;
  private static readonly MIN_EF = 1.3;
  private static readonly MAX_INTERVAL = 180;

  static schedule(
    quality: number,           // 0-5 的回答质量评分
    previousInterval: number,  // 上次间隔
    repetition: number,        // 连续正确次数
    previousEf: number = SpacedRepetitionEngine.DEFAULT_EF,
  ): ReviewResult {
    // 回答不合格 → 重置进度
    if (quality < 3) {
      return {
        nextInterval: 1,
        newRepetition: 0,
        newEf: SpacedRepetitionEngine.updateEf(previousEf, quality),
        nextReview: new Date(Date.now() + 86400000),  // 明天
      };
    }

    // 回答合格 → 正常排程
    const newEf = SpacedRepetitionEngine.updateEf(previousEf, quality);

    let nextInterval: number;
    if (repetition === 0) {
      nextInterval = 1;
    } else if (repetition === 1) {
      nextInterval = 3;
    } else {
      nextInterval = Math.round(previousInterval * newEf);
    }

    nextInterval = Math.min(nextInterval, SpacedRepetitionEngine.MAX_INTERVAL);

    return {
      nextInterval,
      newRepetition: repetition + 1,
      newEf,
      nextReview: new Date(Date.now() + nextInterval * 86400000),
    };
  }
}

回答质量评分(quality)对照表:

评分 含义 处理逻辑
0 完全忘记 重置 → 明天复习
1 错误但能回忆部分 重置 → 明天复习
2 错误但感觉容易 重置 → 明天复习
3 正确但有困难 间隔增长
4 正确但稍有犹豫 间隔增长
5 完全正确且流利 间隔增长

间隔增长算法(repetition >= 2):

newInterval = previousInterval × EF

其中 EF 在每次复习后更新:

EF' = EF + (0.1 - (5 - q) × (0.08 + (5 - q) × 0.02))

这个公式是 SM-2 的核心:如果用户总是能正确回答(quality 高),EF 增长缓慢但稳定,间隔时间不断延长;如果用户经常答错,EF 下降,间隔缩短,复习更频繁。

13.3 掌握度可视化

static getMasteryColor(mastery: number): string {
  if (mastery >= 0.8) return '#00b894';   // 绿色 - 已掌握
  if (mastery >= 0.5) return '#ff9f43';   // 橙色 - 学习中
  if (mastery >= 0.2) return '#e17055';   // 浅红 - 需加强
  return '#d63031';                         // 红色 - 新词/陌生
}

static getMasteryLabel(mastery: number): string {
  if (mastery >= 0.8) return '已掌握';
  if (mastery >= 0.5) return '学习中';
  if (mastery >= 0.2) return '需加强';
  return '新词';
}

四个等级对应四种颜色和标签,可以用在单词列表的左侧指示条或进度圆环中,一目了然。


14. @State 响应式状态管理

@State 是 ArkTS 响应式系统的核心。当 @State 变量发生变化时,框架会自动重新渲染依赖该变量的 UI 部分。

14.1 @State 的基本规则

@State private currentBarState: BarState = BarState.Auto;
@State private currentOffset: string = '0vp';
@State private itemCount: number = 30;
@State private contentHeight: string = '计算中...';

规则:

  1. 必须使用 private 修饰@State 只在组件内部访问
  2. 必须初始化:不给初始值会编译报错
  3. 赋值即触发更新this.itemCount = newCount → 框架立即重渲染
  4. 复杂类型也能响应:数组、对象等引用类型只要重新赋值就会触发

14.2 @State 与 UI 的绑定

@State 变量可以直接在 build() 中渲染,框架会自动追踪依赖关系:

text(`偏移:${this.currentOffset}`)
// 当 this.currentOffset 变化时,只有这行 Text 会重新渲染

Text(`${this.itemCount}`)
// 当 this.itemCount 变化时,这行 Text 也会自动更新

.scrollBar(this.currentBarState)
// 当 this.currentBarState 变化时,Scroll 的滚动条模式切换

增量更新:框架不会重建整个页面,只会更新依赖了变化状态的那部分 UI。这是声明式框架相比传统命令式的核心性能优势。

14.3 状态与事件的交互模式

典型的"事件 → 状态 → UI"链路:

用户点击按钮
  → onClick 回调
    → 修改 @State 变量
      → 框架检测到变化
        → 重新执行 build() 中依赖该变量的部分
          → UI 更新

以我们的增加列表功能为例:

// Step 1: 用户点击 "+ 增 5 条" 按钮
Button('+ 增 5 条')
  .onClick(() => {
    // Step 2: 修改 @State 变量
    this.addItems();
  })

// Step 3: 状态更新方法
private addItems(): void {
  const newCount = Math.min(this.itemCount + 5, this.fullData.length);
  this.itemCount = newCount;      // ← @State 变化
  this.contentHeight = `${newCount * 150}vp+`;

  // Step 4: 交互反馈
  promptAction.showToast({
    message: `增加至 ${newCount} 条内容`,
    duration: 1500
  });
}

14.4 状态与列表渲染

itemCount 变化时,getDisplayData() 返回的数组长度改变,ForEach 只增加/减少对应的列表项组件,已有项复用不变:

private getDisplayData(): ListEntry[] {
  return this.fullData.slice(0, this.itemCount);
}

fullData 不是 @State,因为它从不变化(相当于常量池)。只有 itemCount 是响应式的,控制着"切片"的大小。


15. Canvas 绘图:圆形进度条的自绘实现

ProgressRing 组件使用 ArkUI 的 Canvas 组件实现了圆形进度条,展示了 ArkTS 绘图能力。

15.1 Canvas 基础

private ringContext: CanvasRenderingContext2D = new CanvasRenderingContext2D();
private progressContext: CanvasRenderingContext2D = new CanvasRenderingContext2D();

build() {
  Stack() {
    Canvas(this.ringContext)       // 背景圆环
      .width(this.ringSize)
      .height(this.ringSize)
      .onReady(() => {
        this.drawRing(this.ringContext, 100, true);
      })

    Canvas(this.progressContext)   // 进度弧
      .width(this.ringSize)
      .height(this.ringSize)
      .onReady(() => {
        this.drawRing(this.progressContext, this.ringProgress, false);
      })

    Column() {                     // 中心文字
      Text(this.ringValue).fontSize(16).fontWeight(FontWeight.Bold)
      Text(this.ringLabel).fontSize(10).fontColor('#888').margin({ top: 2 })
    }
  }
}

15.2 绘图逻辑

drawRing(ctx: CanvasRenderingContext2D, value: number, isBg: boolean): void {
  const size = this.ringSize;
  const stroke = this.ringStroke;
  const cx = size / 2;
  const cy = size / 2;
  const r = (size - stroke) / 2;
  const startAngle = -Math.PI / 2;              // 从顶部开始(12 点钟方向)
  const endAngle = startAngle + (value / 100) * 2 * Math.PI;

  ctx.beginPath();
  ctx.arc(cx, cy, r, startAngle, endAngle);
  ctx.strokeStyle = isBg ? this.ringBgColor : this.ringColor;
  ctx.lineWidth = stroke;
  ctx.lineCap = 'round';
  ctx.stroke();
}

绘图要点:

  1. 双 Canvas 分层绘制:背景圆环使用一个 Canvas,进度弧使用另一个 Canvas——这样进度变化时只需重新绘制进度弧,背景无需重绘
  2. 起始角度选择-Math.PI / 2(-90°)使弧线从顶部开始绘制,更符合进度条的习惯
  3. lineCap: 'round':端点圆角,避免进度弧两端出现尖锐的直角
  4. onReady 回调:Canvas 准备就绪后才会触发绘图,防止在 Canvas 未初始化时绘图导致错误

这种"两个 Canvas + Stack 叠加"的模式,在实现圆形进度条时比使用单个 Canvas 更高效,因为进度变化时只触发 progressContextonReadyringContext 保持不变。


16. 性能优化与最佳实践

16.1 ForEach 的键值策略

反例(性能差):

ForEach(
  this.dataList,
  (item) => { ItemCard({ item }) },
  (item) => Math.random().toString()  // 每次都不一样,所有组件都重建
)

正例(性能优):

ForEach(
  this.getDisplayData(),
  (item) => { ListItemCard({ item: item }) },
  (item: ListEntry) => item.id.toString()  // 稳定唯一标识
)

稳定键值让框架可以复用已有组件实例,避免销毁重建的开销。

16.2 避免不必要的状态更新

不必要的状态更新会导致额外的渲染周期。一个常见的优化是使用条件判断:

// 优化前:每次滑动都 setState
.onScroll((x, y) => {
  this.currentOffset = `${Math.round(y)}vp`;
})

// 优化思路:只在变化幅度超过阈值时才更新
// 由于我们的应用需要实时位置显示,保留高频更新

对于不需要实时显示的场景,建议使用 onScrollStop 而不是 onScroll

16.3 组件粒度控制

组件拆分越细,单次更新的范围越小。但也不是越细越好,需要在可维护性和性能之间平衡:

过粗:一个组件包含整个页面 → 任何小变化都导致全量重渲染
过细:每个 Text 都是一个独立组件 → 组件间通信成本高
适中:按功能模块拆分,每个模块 30-100 行

我们的应用中,ColumnScrollPage 约 600 行,内部通过 ListItemCardScrollControlBar 等子组件分解,粒度适中。

16.4 列表性能的 scale 策略

对于超长列表(1000+ 条),Scroll + Column 模式会一次性渲染所有子组件,性能较差。此时应该使用 LazyForEach 代替 ForEach,实现虚拟列表——只渲染可视区域内的组件:

// 适用于超长列表
LazyForEach(this.dataSource, (item: ListEntry) => {
  ListItemCard({ item: item })
}, (item: ListEntry) => item.id.toString())

LazyForEach 需要一个实现了 IDataSource 接口的数据源对象,它会在列表滚动时按需创建和回收组件。

16.5 关于模块化路径的注意事项

在我们的项目中,使用 @State 时需要注意模块化路径。例如,组件属性的命名要避开 ArkUI 内部关键字:

// 正确:使用业务语义的名字
@Prop cardColor: string = '#ffffff';
@Prop cardRadius: number = 16;

// 错误:与 CommonAttribute 方法重名
// @Prop backgroundColor: string = '#ffffff';
// @Prop borderRadius: number = 16;

16.6 hilog 调试的最佳实践

const TAG = 'ScrollColumnDemo';

// 使用 TAG 区分模块
hilog.info(0x0000, TAG, 'Scroll stopped at: %s', this.currentOffset);
hilog.info(0x0000, TAG, 'Card clicked: %s', this.item.title);

// 错误日志带 JSON 格式化
hilog.error(0x0000, TAG, 'Failed: %{public}s', JSON.stringify(error));

hilog 是鸿蒙的日志系统,支持格式化输出和隐私标记(%{public}s / %{private}s)。建议为每个模块定义独立的 TAG,便于日志过滤。


17. 总结与展望

17.1 架构全景回顾

通过本文,我们从零到一拆解了一个鸿蒙 NEXT 应用的全貌:

┌──────────────────────────────────────────────────────┐
│                    应用架构全景                        │
├──────────┬───────────────────────────────────────────┤
│ UI 层    │ @Component / @Entry 组件树               │
│          │ Column + Scroll + Stack 布局容器          │
│          │ ForEach 列表渲染                          │
├──────────┼───────────────────────────────────────────┤
│ 交互层   │ onClick 事件处理                          │
│          │ @State 响应式状态管理                     │
│          │ Scroller 编程式滚动                       │
├──────────┼───────────────────────────────────────────┤
│ 数据层   │ Interface / Enum 数据模型                │
│          │ SampleData 样例数据                       │
│          │ SM-2 间隔重复算法                         │
├──────────┼───────────────────────────────────────────┤
│ 能力层   │ Ability 生命周期                         │
│          │ prompAction 交互反馈                      │
│          │ hilog 日志系统                            │
└──────────┴───────────────────────────────────────────┘

17.2 核心知识点速查表

知识点 关键方法/属性 使用场景
Column 布局 justifyContent() alignItems() 垂直排列内容
弹性撑满 layoutWeight(1) 分段式布局中间撑满
滚动容器 Scroll(scroller) { Column() {} } 超屏内容展示
编程式滚动 scrollTo() scrollEdge() 回到顶部/底部
滚动条控制 scrollBar(BarState) 切换显示模式
边界效果 edgeEffect(EdgeEffect.Spring) 弹簧回弹
响应式数据 @State 状态驱动 UI 更新
组件属性 @Prop 父子组件传值
内容插槽 @BuilderParam 自定义卡片内容
列表渲染 ForEach(array, fn, keyFn) 数据列表展示
Canvas 绘图 CanvasRenderingContext2D 自定义图形绘制
交互反馈 promptAction.showToast() 用户操作即时提示

17.3 展望

HarmonyOS NEXT 的 ArkTS / ArkUI 技术栈仍处于快速演进阶段,未来值得关注的方向包括:

  1. 状态管理进阶:随着应用规模增长,需要引入类似 Redux/Pinia 的全局状态管理方案。目前社区正在探索基于 @Provide / @Consume 装饰器的跨组件状态传递模式。

  2. 自定义组件库建设:本文展示的 CardProgressRingAppHeader 等组件只是起点,企业级应用需要更完善的组件库体系。

  3. 动画与手势:ArkUI 提供了 animateTotransition 等动画 API,以及 PanGestureSwipeGesture 等手势识别,可用于构建更丰富的交互体验。

  4. 端侧 AI 集成:鸿蒙 NEXT 提供了端侧 AI 能力,结合本文的学习数据,可以实现智能化的学习路径推荐。

  5. 多设备适配:鸿蒙的"一次开发,多端部署"理念要求代码能适配手机、平板、折叠屏等多种形态,ArkUI 的栅格系统 (GridRow/GridCol) 是关键的适配手段。


附录:完整术语对照表

术语 说明 类似概念
ArkTS 鸿蒙原生应用开发语言,基于 TypeScript Kotlin、Swift
ArkUI 鸿蒙声明式 UI 框架 SwiftUI、Jetpack Compose
Ability 应用能力的封装,类似 Activity/Page Android Activity
@Component 组件装饰器 @Composablestruct
@State 组件内部响应式状态 useStateMutableState
@Prop 父组件传入的不可变属性 React props
@BuilderParam 内容插槽装饰器 Vue slot、children
Column 垂直布局容器 flex-direction: column
Row 水平布局容器 flex-direction: row
Scroll 滚动容器 ScrollView
ForEach 列表渲染函数 Array.map()v-for
layoutWeight 弹性权重类似 flex flex: 1
Blank 弹性占位组件 Spacer、空 View
Stack 层叠布局容器 FrameLayout
hilog 日志系统 console.log、Logcat
prompAction 原生交互反馈 Toast、Dialog
vp 虚拟像素,自适应单位 dp、pt

本文基于 HarmonyOS NEXT 6.1.1(API 24),SDK 版本 targetSdkVersion: "6.1.1(24)",开发环境为 DevEco Studio。文中所有代码均来源于实际可运行的项目,可在相应地目录下找到完整源码。


Logo

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

更多推荐