1. 先搞清楚:装饰器到底是什么

在 ArkTS/ArkUI 中,装饰器通常以 @ 开头:

@Entry
@Component
struct Index {
  @State count: number = 0

  build() {
    Text(`${this.count}`)
  }
}

它们不是普通函数调用,而是在告诉 ArkUI 框架:
• 这个 struct 是不是页面或自定义组件;
• 这个变量是不是会驱动 UI 刷新;
• 这个变量从父组件、全局存储还是页面存储中来;
• 父子组件之间的数据能否双向同步;
• 一段 UI 是否可复用、可传入、可监听;
• 一个对象的深层属性变化是否要被观察。
可以把装饰器理解成“给类、组件、字段或方法贴上的框架规则标签”。

普通变量
  count: number = 0

加上 @State 后
  @State count: number = 0
       ↑
  告诉 ArkUI:count 改变时,依赖它的 UI 需要刷新

2. 重要总览:装饰器分为哪几类

先看地图,再逐个学习。
在这里插入图片描述
在这里插入图片描述

第一部分:页面与自定义组件装饰器

3. @Entry:声明页面入口

3.1 它做什么

@Entry 表示一个组件是页面入口。应用启动或路由跳转后,系统会显示这个入口组件。

@Entry
@Component
struct Index {
  build() {
    Text('这是首页')
  }
}

3.2 适用位置

• 放在页面级组件上,例如 Index.ets、HomePage.ets。
• 一般和 @Component 一起使用。
• 通常一个页面文件只定义一个入口组件。

3.3 完整可运行示例

@Entry
@Component
struct Index {
  build() {
    Column() {
      Text('欢迎来到 ArkUI')
        .fontSize(26)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

3.4 注意事项

  1. @Entry 不等于“任意可复用组件”。它强调页面入口。
  2. 普通子组件通常只需要 @Component,不要都加 @Entry。
  3. 页面配置、路由配置与页面文件之间需要匹配;如果路由不到页面,优先检查工程配置和页面注册。
  4. 可通过 @Entry({…}) 接收页面级 LocalStorage 等参数,具体形态随 SDK 版本而变化,初学先掌握无参数写法。

4. @Component:定义 V1 自定义组件

4.1 它做什么

@Component 将一个 struct 标记为 ArkUI 自定义组件。组件必须提供 build() 方法描述 UI。

@Component
struct WelcomeCard {
  build() {
    Text('欢迎学习自定义组件')
  }
}

使用:

WelcomeCard()

4.2 最小模板

@Component
struct ComponentName {
  build() {
    // ArkUI 组件树
  }
}

4.3 完整示例:页面使用自定义组件

@Component
struct WelcomeCard {
  @Prop name: string = '同学'

  build() {
    Column({ space: 8 }) {
      Text(`你好,${this.name}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Text('今天继续练习 ArkUI 组件。')
        .fontSize(14)
        .fontColor('#666666')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#F5F7FA')
    .borderRadius(12)
  }
}

@Entry
@Component
struct Index {
  build() {
    Column() {
      WelcomeCard({ name: '小明' })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

4.4 注意事项

  1. @Component 是 V1 状态管理体系 的核心组件装饰器。
  2. 组件字段若需要驱动 UI 更新,需要使用相应状态装饰器,例如 @State;普通字段不具备响应式刷新能力。
  3. 组件名用大驼峰命名,例如 CourseCard、ProfileHeader。
  4. build() 主要负责描述 UI;不要在 build() 中无条件修改状态。
  5. 一个组件应有清楚职责:例如“课程卡片”“数量选择器”,不要使用无意义的 Box1、View2。

5. @Preview:在 IDE 中预览组件

5.1 它做什么

@Preview 用于标记可以在 DevEco Studio 中进行预览的组件或预览入口。它的主要价值是:不必完整启动应用,也能快速观察组件样式。
常见形式:

@Preview
@Component
struct PreviewCard {
  build() {
    Text('组件预览')
  }
}

5.2 建议用法

@Component
struct CourseCard {
  @Prop title: string = ''

  build() {
    Text(this.title)
      .fontSize(18)
      .padding(16)
  }
}

@Preview
@Component
struct CourseCardPreview {
  build() {
    CourseCard({ title: 'ArkUI 自定义组件' })
  }
}

5.3 注意事项

  1. @Preview 的具体能力与 IDE、SDK、工程配置密切相关;若当前 IDE 不显示预览入口,应以代码运行结果为准。
  2. 预览数据应尽量使用固定的模拟数据,不依赖网络、数据库、权限或复杂页面路由。
  3. @Preview 是开发辅助,不是用户运行时页面入口;不能替代 @Entry。
  4. 不同版本可能支持不同的预览参数,优先使用 IDE 自动补全生成的当前版本写法。

6. @Reusable:声明可复用组件

6.1 它做什么

@Reusable 用于提示框架:该自定义组件适合在 UI 复用场景中被复用,常见于列表、大量重复卡片等对性能较敏感的场景。

@Reusable
@Component
struct ReusableCourseRow {
  @Prop title: string = ''

  build() {
    Text(this.title)
      .padding(16)
  }
}

6.2 适合场景

• List、Grid 中大量重复的列表项。
• 结构相同、只有数据不同的卡片。
• 高频滚动、创建和销毁组件较多的页面。

6.3 注意事项

  1. 不要把 @Reusable 当成普通组件的必写项;只有明确存在复用/性能诉求时才使用。
  2. 可复用组件的状态设计要更谨慎:不要假设组件每次出现都一定是全新实例。
  3. 避免把一次性业务流程、复杂页面级状态塞到可复用列表项内部。
  4. 是否可用及具体复用规则以当前 SDK 文档为准。初学项目先保证正确性,再考虑复用优化。

7. @CustomDialog:定义自定义弹窗组件

7.1 它做什么

@CustomDialog 用于定义由 CustomDialogController 控制的自定义弹窗内容。

@CustomDialog
struct ConfirmDialog {
  controller?: CustomDialogController

  build() {
    Column() {
      Text('确认操作?')
      Button('关闭')
        .onClick(() => {
          this.controller?.close();
        })
    }
  }
}

7.2 完整可运行示例

@CustomDialog
struct ConfirmDialog {
  controller?: CustomDialogController
  message: string = '确定要继续吗?'
  onConfirm: () => void = () => {}

  build() {
    Column({ space: 16 }) {
      Text('操作确认')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Text(this.message)
        .fontSize(16)

      Row({ space: 12 }) {
        Button('取消')
          .layoutWeight(1)
          .onClick(() => {
            this.controller?.close();
          })

        Button('确定')
          .layoutWeight(1)
          .onClick(() => {
            this.onConfirm();
            this.controller?.close();
          })
      }
      .width('100%')
    }
    .width('80%')
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }
}

@Entry
@Component
struct Index {
  @State result: string = '尚未操作'

  private dialogController: CustomDialogController = new CustomDialogController({
    builder: ConfirmDialog({
      message: '是否把“学习装饰器”标记为已完成?',
      onConfirm: () => {
        this.result = '已确认完成';
      }
    })
  })

  build() {
    Column({ space: 16 }) {
      Text(this.result)
        .fontSize(22)

      Button('打开自定义弹窗')
        .onClick(() => {
          this.dialogController.open();
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

7.3 注意事项

  1. @CustomDialog 修饰的是弹窗内容结构,不是普通页面。
  2. 弹窗通常通过 CustomDialogController 打开和关闭。
  3. 使用可选链 this.controller?.close() 可避免控制器尚未注入时直接报错。
  4. 不要在弹窗中长期持有页面或 Context 的不必要引用,避免生命周期混乱。
  5. 对删除、退出、清空等高风险操作,弹窗应清楚说明后果,并提供“取消”入口。
  6. 部分 SDK 对 CustomDialogController 的构造参数、传参方式有更新;如 IDE 报类型错误,优先让 IDE 自动生成当前 SDK 的基础模板。

8. 状态管理的核心:数据改变,UI 自动刷新

ArkUI 是声明式 UI:

Text(`计数:${this.count}`)

页面显示依赖 count。只有当 count 是可观察状态时,改变它才能触发 UI 更新:

@State count: number = 0

最常见的数据流:

父组件的 @State
      ↓
@Prop / @Link / 回调
      ↓
子组件显示或触发操作
      ↓
父组件更新状态
      ↓
依赖该状态的 UI 刷新

9. @State:组件内部拥有的状态

9.1 它做什么

@State 修饰组件内部状态。状态改变时,依赖它的 UI 会重新渲染。

@State count: number = 0

9.2 完整可运行示例

@Entry
@Component
struct Index {
  @State count: number = 0
  @State message: string = '还没有点击按钮'

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

      Text(this.message)
        .fontSize(16)
        .fontColor('#666666')

      Button('+1')
        .width('100%')
        .onClick(() => {
          this.count++;
          this.message = `你已经点击了 ${this.count} 次`;
        })

      Button('重置')
        .width('100%')
        .onClick(() => {
          this.count = 0;
          this.message = '计数已重置';
        })
    }
    .width('100%')
    .height('100%')
    .padding(24)
    .justifyContent(FlexAlign.Center)
  }
}

9.3 适合什么数据

• 输入框当前文本。
• 是否登录、是否展开、是否加载中。
• 当前选中项、当前页码。
• 当前组件自己拥有的列表。
• 用户点击按钮后会改变的计数值。

9.4 注意事项

  1. @State 只适合“当前组件拥有”的状态。
  2. 在子组件中直接修改来自父组件的数据,不应靠复制一份 @State 来“假装同步”;应根据场景选 @Prop、@Link 或回调。
  3. 对对象、数组的深层变更要尤其注意响应式规则。简单入门中,推荐生成新对象/新数组后重新赋值:
this.todos = [...this.todos, newTodo];

this.todos = this.todos.map((item: Todo) => {
  return item.id === id ? { ...item, done: !item.done } : item;
});
  1. 不要在 build() 内无条件执行 this.count++、网络请求、修改 @State。这可能导致重复构建或循环刷新。
  2. @State 不是数据库,也不是永久存储。应用重启后需要保留的数据,应使用持久化方案。

10. @Prop:父组件向子组件单向传值

10.1 它做什么

@Prop 用于子组件接收父组件传来的数据,主要用于展示。

@Component
struct UserName {
  @Prop name: string = '访客'

  build() {
    Text(`你好,${this.name}`)
  }
}

父组件:

UserName({ name: '小明' })

10.2 完整示例

interface Course {
  title: string;
  description: string;
  finished: boolean;
}

@Component
struct CourseCard {
  @Prop course: Course = {
    title: '',
    description: '',
    finished: false
  }

  build() {
    Column({ space: 8 }) {
      Text(this.course.title)
        .fontSize(19)
        .fontWeight(FontWeight.Bold)

      Text(this.course.description)
        .fontSize(14)
        .fontColor('#666666')

      Text(this.course.finished ? '已完成' : '学习中')
        .fontSize(13)
        .fontColor(this.course.finished ? '#2E8B57' : '#E67E22')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#F7F8FA')
    .borderRadius(12)
  }
}

@Entry
@Component
struct Index {
  @State course: Course = {
    title: 'ArkUI 装饰器',
    description: '学习 @State、@Prop 与 @Link。',
    finished: false
  }

  build() {
    Column({ space: 12 }) {
      CourseCard({ course: this.course })

      Button('切换完成状态')
        .onClick(() => {
          this.course = {
            title: this.course.title,
            description: this.course.description,
            finished: !this.course.finished
          };
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

10.3 注意事项

  1. 把 @Prop 当作“父组件给子组件的输入”,而不是子组件的私有可变状态。
  2. 基本类型、对象、数组均可传递;但复杂对象的变更规则与 SDK 版本有关,展示型子组件应尽量保持只读。
  3. 子组件需要通知父组件发生操作时,优先传回调函数:
    onDelete: (id: number) => void = () => {}
  4. 不要依赖子组件修改 @Prop 来更新父组件。需要双向绑定时用 @Link,或使用“回调 + 父组件更新 @State”。
  5. 给 @Prop 写合理默认值,便于预览与容错;但关键业务数据仍应由父组件明确传入。

11. @Link:父子组件双向状态链接

11.1 它做什么

@Link 用于让子组件与父组件的状态建立双向链接。子组件修改这个值,父组件对应状态也会变化。
子组件:

@Component
struct CounterControl {
  @Link count: number

  build() {
    Button(`+1:${this.count}`)
      .onClick(() => {
        this.count++;
      })
  }
}

父组件:

@State count: number = 0

CounterControl({ count: $count })

11.2 完整可运行示例

@Component
struct QuantityStepper {
  @Prop label: string = '数量'
  @Link value: number

  build() {
    Row({ space: 16 }) {
      Text(this.label)
        .fontSize(18)
        .layoutWeight(1)

      Button('-')
        .onClick(() => {
          if (this.value > 0) {
            this.value--;
          }
        })

      Text(`${this.value}`)
        .fontSize(22)
        .width(48)
        .textAlign(TextAlign.Center)

      Button('+')
        .onClick(() => {
          this.value++;
        })
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#F7F8FA')
    .borderRadius(12)
    .alignItems(VerticalAlign.Center)
  }
}

@Entry
@Component
struct Index {
  @State appleCount: number = 1
  @State orangeCount: number = 2

  build() {
    Column({ space: 16 }) {
      Text(`总水果数:${this.appleCount + this.orangeCount}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      QuantityStepper({
        label: '苹果',
        value: $appleCount
      })

      QuantityStepper({
        label: '橙子',
        value: $orangeCount
      })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

11.3 注意事项

  1. 父组件传给 @Link 时,必须传状态变量的链接形式,例如 $count。
  2. @Link 适用于真正的双向编辑,例如数量选择器、开关、编辑表单。
  3. 不要用 @Link 代替所有回调。数据可被任意子组件直接修改,会让大型项目的数据流难追踪。
  4. 列表项的“删除”“跳转详情”等一次性事件,通常更推荐回调;简单字段编辑才更适合 @Link。
  5. 子组件没有有效 @Link 输入时,不应把它当作完全独立的可复用组件使用。

12. @Provide 与 @Consume:跨层级共享状态

12.1 它们做什么

当父组件与目标子组件之间隔了很多层,层层使用 @Prop 传递会很麻烦。@Provide 可以向后代提供状态,@Consume 可以在后代组件中消费该状态。
祖先组件:

@Provide themeName: string = '浅色模式'

后代组件:

@Consume themeName: string

12.2 完整可运行示例

@Component
struct DeepChild {
  @Consume userName: string

  build() {
    Text(`最深层组件收到:${this.userName}`)
      .fontSize(18)
      .padding(16)
      .backgroundColor('#E8F3FF')
      .borderRadius(12)
  }
}

@Component
struct MiddleLayer {
  build() {
    Column({ space: 12 }) {
      Text('我是中间组件,不需要手动转发 userName。')
        .fontSize(14)
        .fontColor('#666666')

      DeepChild()
    }
    .width('100%')
  }
}

@Entry
@Component
struct Index {
  @Provide userName: string = '小明'

  build() {
    Column({ space: 16 }) {
      TextInput({ placeholder: '修改用户名', text: this.userName })
        .onChange((value: string) => {
          this.userName = value;
        })

      MiddleLayer()
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

12.3 注意事项

  1. @Provide / @Consume 解决“跨层级传值”,不是全局状态管理的万能替代品。
  2. 提供者与消费者必须处在同一组件树的祖先/后代关系中。
  3. 多个同名提供者存在时,离消费者更近的提供者通常会产生遮蔽效果;命名要清楚。
  4. 共享数据过多会让来源不透明。跨层级共享少量主题、用户偏好、上下文信息较合适。
  5. 用于复杂业务共享状态时,要明确谁拥有和修改数据,避免多个消费者随意写入造成调试困难。
  6. V2 中对应思路是 @Provider / @Consumer,二者不能简单混为一谈。

13. @Observed:让类对象具备可观察能力

13.1 为什么需要它

@State 修饰基本数据时很直观:
@State count: number = 0
但如果状态是“类实例”,并且你希望其中属性变化也能被框架观察,就需要 @Observed 修饰该类,再配合 @ObjectLink 使用。

@Observed
class Todo {
  title: string;
  done: boolean;

  constructor(title: string, done: boolean) {
    this.title = title;
    this.done = done;
  }
}

13.2 典型写法

@Observed
class Person {
  name: string = '小明';
  age: number = 18;
}

13.3 注意事项

  1. @Observed 修饰的是类,不是 struct 组件字段。
  2. 它属于 V1 深层对象响应式机制,常与 @ObjectLink 配套。
  3. 不要为了所有普通数据都定义 class + @Observed;简单数据可用接口和不可变更新方式。
  4. 复杂嵌套对象、数组元素响应式容易增加理解成本。初学项目可优先采用“新对象/新数组重新赋值”的方式。
  5. V2 对应更精细的对象观察机制是 @ObservedV2 + @Trace。

14. @ObjectLink:链接可观察对象

14.1 它做什么

@ObjectLink 用于子组件接收并链接一个由 @Observed 修饰的对象。对象属性变化时,相关 UI 可以响应更新。
@ObjectLink todo: Todo

14.2 完整可运行示例

@Observed
class Todo {
  id: number;
  title: string;
  done: boolean;

  constructor(id: number, title: string, done: boolean) {
    this.id = id;
    this.title = title;
    this.done = done;
  }
}

@Component
struct TodoRow {
  @ObjectLink todo: Todo

  build() {
    Row({ space: 12 }) {
      Text(this.todo.title)
        .fontSize(18)
        .layoutWeight(1)
        .decoration({
          type: this.todo.done ? TextDecorationType.LineThrough : TextDecorationType.None
        })

      Button(this.todo.done ? '恢复' : '完成')
        .onClick(() => {
          this.todo.done = !this.todo.done;
        })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#F7F8FA')
    .borderRadius(10)
  }
}

@Entry
@Component
struct Index {
  @State todo: Todo = new Todo(1, '练习 @Observed 和 @ObjectLink', false)

  build() {
    Column({ space: 16 }) {
      TodoRow({ todo: this.todo })

      Text(this.todo.done ? '页面状态:已完成' : '页面状态:未完成')
        .fontSize(17)
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

14.3 注意事项

  1. @ObjectLink 的对象类型应是 @Observed 类实例。
  2. 它适合子组件需要读写某个对象属性的场景。
  3. 不要把普通 interface 对象直接当作 @ObjectLink 的对象使用。
  4. 使用对象引用传递时,组件之间耦合更强。对于“子组件只展示数据”的场景,仍优先使用 @Prop。
  5. 多层嵌套与大量对象联动时,建议评估是否迁移到 V2 状态管理,或使用更清晰的不可变更新策略。

15. @Watch:监听状态变量变化

15.1 它做什么

@Watch 用于指定一个方法:当被装饰状态变量发生变化时,该方法会被调用。

@State @Watch('onCountChanged') count: number = 0

private onCountChanged(): void {
  console.info(`count 变为 ${this.count}`);
}

15.2 完整可运行示例

@Entry
@Component
struct Index {
  @State @Watch('onInputChanged') inputText: string = ''
  @State tip: string = '请开始输入'

  private onInputChanged(): void {
    if (this.inputText.trim().length === 0) {
      this.tip = '请输入至少一个字符';
    } else if (this.inputText.length < 6) {
      this.tip = '输入内容较短';
    } else {
      this.tip = '输入长度合适';
    }
  }

  build() {
    Column({ space: 14 }) {
      TextInput({ placeholder: '请输入学习计划', text: this.inputText })
        .onChange((value: string) => {
          this.inputText = value;
        })

      Text(this.tip)
        .fontSize(16)
        .fontColor('#666666')

      Text(`当前字符数:${this.inputText.length}`)
        .fontSize(14)
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

15.3 注意事项

  1. @Watch(‘方法名’) 中是方法名字符串,方法需要定义在同一组件内。
  2. 监听方法中再次修改被监听的同一状态,可能形成重复触发或逻辑循环;必须设计清楚终止条件。
  3. @Watch 适合执行“状态变化后的附加动作”,例如输入校验、记录日志、触发派生更新。
  4. 如果一个结果可直接由其他状态计算得到,优先在 build() 的表达式或专门计算方法中得到,而不是滥用 @Watch 保存重复状态。
  5. 不要在 @Watch 中执行难以控制的高频网络请求;输入状态变化很频繁时应考虑防抖等策略。

第三部分:V1 存储连接装饰器

16. 先区分:AppStorage 与 LocalStorage 不是装饰器

它们是状态存储容器;真正加在组件字段上的才是装饰器。
在这里插入图片描述
AppStorage 适合整个应用需要共享的 UI 状态,例如主题、登录显示状态、语言偏好。
LocalStorage 适合同一页面及其组件树共享的 UI 状态。
需要跨应用重启保留的数据,不是“只用一个装饰器”就完成,通常还要配置持久化 API、Preferences 或数据库。

17. @StorageProp:读取 AppStorage 中的值

17.1 它做什么

@StorageProp(‘key’) 将组件字段与 AppStorage 的指定键建立单向同步:存储中的值变化,组件字段和 UI 更新;组件不应通过该字段反向修改存储。

@StorageProp('themeName') themeName: string = '浅色模式'

17.2 完整可运行示例

@Entry
@Component
struct Index {
  @StorageProp('globalUserName') userName: string = '访客'
  @State inputName: string = ''

  aboutToAppear(): void {
    this.inputName = this.userName;
  }

  build() {
    Column({ space: 14 }) {
      Text(`全局用户名:${this.userName}`)
        .fontSize(22)

      TextInput({ placeholder: '请输入全局用户名', text: this.inputName })
        .onChange((value: string) => {
          this.inputName = value;
        })

      Button('写入 AppStorage')
        .onClick(() => {
          AppStorage.setOrCreate('globalUserName', this.inputName.trim() || '访客');
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

17.3 注意事项

  1. 键名必须统一管理,例如 ‘globalUserName’,避免不同模块拼错键名。
  2. @StorageProp 是“存储到组件”的单向输入;若需要通过字段直接写回存储,使用 @StorageLink。
  3. 字段默认值只在键不存在或初始场景中起作用;不要误以为它能强制覆盖已有存储值。
  4. AppStorage 是运行时 UI 状态容器,不等同于永久持久化。
  5. 不要把密码、令牌、身份证号等敏感数据直接作为普通 UI 全局状态使用。

18. @StorageLink:与 AppStorage 双向同步

18.1 它做什么

@StorageLink(‘key’) 将组件字段与 AppStorage 中的键双向链接。

@StorageLink('fontSize') fontSize: number = 16

字段变化会更新存储,存储变化也会更新字段。

18.2 完整可运行示例

@Entry
@Component
struct Index {
  @StorageLink('globalFontSize') fontSize: number = 18

  build() {
    Column({ space: 16 }) {
      Text('全局字号演示')
        .fontSize(this.fontSize)

      Text(`当前字号:${this.fontSize}`)
        .fontSize(16)

      Row({ space: 12 }) {
        Button('变小')
          .onClick(() => {
            if (this.fontSize > 12) {
              this.fontSize -= 2;
            }
          })

        Button('变大')
          .onClick(() => {
            if (this.fontSize < 32) {
              this.fontSize += 2;
            }
          })
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

18.3 注意事项

  1. @StorageLink 是双向的,写入影响范围比 @State 大;不要用它保存仅属于一个小组件的临时状态。
  2. 多个组件连接同一个键时,任何一处修改都会影响全部连接组件。要有明确所有权和命名规范。
  3. AppStorage 中同一个键的数据类型要保持一致。不要某处按 number 使用、另一处按 string 使用。
  4. @StorageLink 不等于自动永久保存。应用重启后的行为取决于是否配置了持久化能力。
  5. 页面销毁后不再需要的临时键,可根据业务设计清理,避免全局状态无限堆积。

19. @LocalStorageProp:读取页面级 LocalStorage

19.1 它做什么

@LocalStorageProp(‘key’) 连接页面级 LocalStorage 中的一个值,以单向方式更新组件。

@LocalStorageProp('pageTitle') title: string = '默认标题'

19.2 典型用法

页面入口接收或创建 LocalStorage 实例,再让页面后代组件连接同一键。不同 SDK 的入口参数组织方式可能略有差异,使用时建议通过当前 IDE 模板创建。
概念示例:

const pageStorage = new LocalStorage({ pageTitle: '学习中心' });

// 由页面入口或页面组件树使用该存储实例后:
@Component
struct PageHeader {
  @LocalStorageProp('pageTitle') title: string = '默认标题'

  build() {
    Text(this.title)
  }
}

19.3 注意事项

  1. 它只适合页面作用域的数据共享,不是应用全局状态。
  2. LocalStorage 实例必须被正确传入页面入口/组件树,否则字段找不到预期数据。
  3. 单向读取场景用 @LocalStorageProp;需要双向写回用 @LocalStorageLink。
  4. 不同页面的 LocalStorage 实例相互独立,除非你显式传递同一个实例。
  5. 由于入口参数细节容易受 SDK 版本影响,真实项目优先参考当前 DevEco Studio 的官方模板和类型提示。

20. @LocalStorageLink:与页面级 LocalStorage 双向同步

20.1 它做什么

@LocalStorageLink(‘key’) 与 LocalStorage 的页面级状态双向链接。

@LocalStorageLink('searchKeyword') keyword: string = ''

20.2 典型片段

@Component
struct SearchBar {
  @LocalStorageLink('searchKeyword') keyword: string = ''

  build() {
    TextInput({ placeholder: '搜索课程', text: this.keyword })
      .onChange((value: string) => {
        this.keyword = value;
      })
  }
}

20.3 注意事项

  1. 使用前要保证该组件处在携带对应 LocalStorage 的页面组件树中。
  2. 它是双向链接;多个组件使用同一键时,输入会实时同步。
  3. 本页面短暂搜索词、筛选条件、标签选择等场景很合适。
  4. 页面离开后是否保留由 LocalStorage 实例生命周期决定,不要把它误认为可靠的长期存储。
  5. 当状态只在一个组件内部存在时,直接使用 @State 更简单。

第四部分:UI 构建与样式声明

这些能力都以 @ 开头,常被初学者归入“装饰器”。它们与状态管理装饰器不同:主要服务于 UI 复用和样式组织。

21. @Builder:定义可复用 UI 构建函数

21.1 它做什么

@Builder 把一段 UI 声明封装成可调用的构建函数。适合在当前组件内部复用小块 UI。

@Entry
@Component
struct Index {
  @Builder
  private sectionTitle(title: string): void {
    Text(title)
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
  }

  build() {
    Column() {
      this.sectionTitle('第一部分')
      this.sectionTitle('第二部分')
    }
  }
}

21.2 完整可运行示例

@Entry
@Component
struct Index {
  @State studyMinutes: number = 20

  @Builder
  private buildTitle(text: string): void {
    Text(text)
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .width('100%')
      .margin({ top: 12, bottom: 8 })
  }

  @Builder
  private buildInfoRow(label: string, value: string): void {
    Row() {
      Text(label)
        .fontSize(15)
        .fontColor('#666666')
        .layoutWeight(1)

      Text(value)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#F7F8FA')
    .borderRadius(10)
  }

  build() {
    Column({ space: 8 }) {
      this.buildTitle('今日学习')
      this.buildInfoRow('学习主题', '鸿蒙装饰器')
      this.buildInfoRow('学习时长', `${this.studyMinutes} 分钟`)

      this.buildTitle('操作')
      Button('增加 10 分钟')
        .width('100%')
        .onClick(() => {
          this.studyMinutes += 10;
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

21.3 注意事项

  1. @Builder 不是一个完整独立组件,通常在所属组件内部调用。
  2. 组件之间需要复用、需要独立状态或需要独立文件时,更适合 @Component。
  3. @Builder 适合 UI 片段,不适合把复杂业务逻辑隐藏得过深。
  4. 参数类型应明确,尤其是对象参数。
  5. 如果构建函数需要访问所属组件状态,注意 this 上下文,按组件内成员函数方式调用最清楚。

22. @BuilderParam:让父组件传入一块 UI(插槽)

22.1 它做什么

@BuilderParam 声明一个由外部传入的 UI 构建函数。它类似“组件插槽”:组件负责外壳,使用者决定内部内容。

@Component
struct CommonCard {
  @BuilderParam content: () => void = this.defaultContent

  @Builder
  private defaultContent(): void {
    Text('暂无内容')
  }

  build() {
    Column() {
      this.content()
    }
  }
}

22.2 完整可运行示例

@Component
struct CommonCard {
  @Prop title: string = ''
  @BuilderParam content: () => void = this.defaultContent

  @Builder
  private defaultContent(): void {
    Text('暂无内容')
      .fontColor('#999999')
  }

  build() {
    Column({ space: 12 }) {
      Text(this.title)
        .fontSize(19)
        .fontWeight(FontWeight.Bold)

      Divider()
        .color('#E5E6EB')

      this.content()
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#F7F8FA')
    .borderRadius(14)
  }
}

@Entry
@Component
struct Index {
  @State message: string = '今天学习 @BuilderParam'

  build() {
    Column({ space: 14 }) {
      CommonCard({
        title: '学习提醒',
        content: () => {
          Text(this.message)
            .fontSize(16)
            .fontColor('#0A59F7')
        }
      })

      CommonCard({
        title: '操作区',
        content: () => {
          Button('更新提醒')
            .width('100%')
            .onClick(() => {
              this.message = '提醒已更新:完成一个可复用卡片。';
            })
        }
      })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

22.3 注意事项

  1. @BuilderParam 适合“固定外壳 + 可变内容”,例如通用卡片、弹窗、列表项容器。
  2. 给它提供默认 @Builder,可让组件在未传内容时有合理显示。
  3. 传入函数时不要立刻执行:
// 正确
content: () => {
  Text('内容')
}

// 不要写成“执行结果”
  1. 如果插槽内部需要大量状态和逻辑,考虑把它拆成独立 @Component,避免嵌套过深。
  2. 父组件传入的 UI 会捕获父组件状态;这很灵活,但要保持数据来源清晰。

23. @Styles:定义可复用样式

23.1 它做什么

@Styles 用于将一组通用样式抽取为可复用样式函数。它主要降低重复链式属性的数量。

@Styles
function cardStyle() {
  .width('100%')
  .padding(16)
  .backgroundColor('#F7F8FA')
  .borderRadius(12)
}

应用:

Column() {
  Text('卡片内容')
}
.cardStyle()

23.2 完整示例

@Styles
function cardStyle() {
  .width('100%')
  .padding(16)
  .backgroundColor('#F7F8FA')
  .borderRadius(12)
}

@Styles
function primaryTextStyle() {
  .fontSize(18)
  .fontWeight(FontWeight.Bold)
  .fontColor('#222222')
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 12 }) {
      Column({ space: 8 }) {
        Text('ArkUI 样式复用')
          .primaryTextStyle()

        Text('使用 @Styles 可以减少重复的样式代码。')
          .fontSize(14)
          .fontColor('#666666')
      }
      .cardStyle()

      Column() {
        Text('第二张卡片')
          .primaryTextStyle()
      }
      .cardStyle()
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

23.3 注意事项

  1. @Styles 适合没有复杂参数、没有强业务语义的通用外观。
  2. 它不是替代 @Component 的方案。需要封装结构、交互和状态时,请使用组件。
  3. 样式函数能调用哪些属性取决于应用目标组件;不要在样式中写某些组件不支持的属性。
  4. 全局样式名称要避免冲突,建议按用途命名,如 cardStyle、pageTitleStyle。
  5. 样式过大、包含太多条件逻辑时,代码反而难读;保持单一职责。

24. @Extend:扩展指定系统组件能力

24.1 它做什么

@Extend 为某种指定系统组件扩展一个可复用方法。它适合封装“某类组件的固定样式/行为”。

@Extend(Text)
function titleStyle() {
  .fontSize(24)
  .fontWeight(FontWeight.Bold)
  .fontColor('#222222')
}

使用:

Text('页面标题')
  .titleStyle()

24.2 完整示例

@Extend(Text)
function pageTitleStyle() {
  .fontSize(26)
  .fontWeight(FontWeight.Bold)
  .fontColor('#222222')
  .width('100%')
}

@Extend(Button)
function primaryButtonStyle() {
  .width('100%')
  .height(44)
  .backgroundColor('#0A59F7')
  .fontColor(Color.White)
  .borderRadius(10)
}

@Entry
@Component
struct Index {
  @State result: string = '等待操作'

  build() {
    Column({ space: 16 }) {
      Text('使用 @Extend 扩展组件')
        .pageTitleStyle()

      Text(this.result)
        .fontSize(16)
        .fontColor('#666666')

      Button('执行操作')
        .primaryButtonStyle()
        .onClick(() => {
          this.result = '操作完成';
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

24.3 @Styles 与 @Extend 的区别

在这里插入图片描述

24.4 注意事项

  1. @Extend(Text) 的函数只能用于 Text,不能随意套给 Button 或 Column。
  2. 扩展名应体现用途而不是颜色,例如 primaryButtonStyle() 比 blueButton() 更稳健。
  3. 不要在 @Extend 中塞进严重依赖页面业务状态的逻辑。
  4. 如果团队里多个模块定义同名扩展,应做好命名空间和文件组织,避免冲突。

25. @AnimatableExtend:为组件扩展可动画属性

25.1 它做什么

@AnimatableExtend 用于自定义可动画的扩展效果。常见于颜色、尺寸、旋转等需要随动画状态平滑变化的场景。
概念示例:

@AnimatableExtend(Text)
function animatedTextColor(color: string) {
  .fontColor(color)
}

使用时通常结合 animateTo 或组件动画能力控制状态变化。

25.2 示例思路

@AnimatableExtend(Text)
function highlightStyle(color: string) {
  .fontColor(color)
}

@Entry
@Component
struct Index {
  @State active: boolean = false

  build() {
    Column({ space: 16 }) {
      Text('点击后变色')
        .fontSize(24)
        .highlightStyle(this.active ? '#0A59F7' : '#666666')

      Button('切换')
        .onClick(() => {
          animateTo({ duration: 300 }, () => {
            this.active = !this.active;
          });
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

25.3 注意事项

  1. 此能力对 SDK 版本、属性是否支持插值动画有要求;如遇编译问题,先检查当前 API 文档和 IDE 类型提示。
  2. 动画内应改变可观察状态,例如 @State active。
  3. 不要为每一个微小样式都创建动画扩展;动画应服务于反馈、层级和过渡,而不是干扰用户。
  4. 动画时长、曲线、可访问性偏好都应考虑;避免过长、频繁、突兀的动画。
  5. 复杂动画应优先使用平台推荐动画 API,而不是堆叠大量扩展函数。

第五部分:状态管理 V2 装饰器

26. 为什么有 V2

V1 已经能解决大量页面开发问题,但对复杂对象、属性级追踪、来源明确性等场景,V2 提供了更精细的模型。
V1 与 V2 的核心对照:
在这里插入图片描述
重要:V1 和 V2 不是“同一个装饰器换名字”。
同一自定义组件内部不要随意混用两套状态管理模型。
V2 的可用性、限制和语法细节必须以项目所使用 SDK 的官方文档、IDE 自动补全为准。

27. @ComponentV2:定义 V2 自定义组件

27.1 它做什么

@ComponentV2 是 V2 状态管理体系中的自定义组件装饰器。

@ComponentV2
struct UserPanel {
  build() {
    Text('V2 组件')
  }
}

27.2 注意事项

  1. @ComponentV2 内应使用 V2 对应状态装饰器,例如 @Local、@Param、@Event 等。
  2. 不要在同一个组件中把 V1 的 @State、@Prop 与 V2 的状态装饰器混写。
  3. 作为初学者,如果项目现有代码都使用 @Component 与 @State,先沿用 V1;没有明确需求不要强行迁移。
  4. V2 在不同 SDK 版本中的稳定性与支持范围可能不同。遇到“装饰器不可用”并不一定是写错,可能是工程 API 版本不匹配。

28. @ObservedV2:声明 V2 可观察类

28.1 它做什么

@ObservedV2 修饰类,表示该类参与 V2 的可观察状态机制。

@ObservedV2
class UserInfo {
  @Trace name: string = '小明'
  @Trace level: number = 1
}

28.2 注意事项

  1. @ObservedV2 修饰类。
  2. 它通常与 @Trace 一起使用;仅仅把类标记为可观察,未必意味着每个字段都自动按你预期追踪。
  3. V2 强调属性级追踪:只标记真正会影响 UI 的字段,避免无意义地把大量字段都变成响应式。
  4. 不要同时用 @Observed 和 @ObservedV2 修饰同一类。

29. @Trace:追踪 V2 类属性变化

29.1 它做什么

@Trace 标记 @ObservedV2 类中需要被响应式追踪的属性。

@ObservedV2
class LearningProgress {
  @Trace completed: number = 0
  @Trace total: number = 10
}

当 completed、total 改变时,依赖它们的 UI 才能精细刷新。

29.2 概念示例

@ObservedV2
class Profile {
  @Trace name: string = '小明'
  @Trace age: number = 18

  // 不影响 UI 的临时说明可不追踪
  internalRemark: string = ''
}

29.3 注意事项

  1. @Trace 主要用于 @ObservedV2 类的字段。
  2. 不要误以为它是日志工具;它的“追踪”指 UI 状态依赖追踪。
  3. UI 依赖哪些字段,就追踪哪些字段;这是 V2 性能与可维护性优势的一部分。
  4. 嵌套对象、数组、Map/Set 等复杂数据结构的追踪粒度和限制应查看当前 SDK 官方说明。
  5. 不要用 @Trace 替代业务审计日志;二者目的完全不同。

30. @Local:V2 组件内部状态

30.1 它做什么

@Local 表示 V2 组件拥有的本地状态,概念上接近 V1 的 @State。

@ComponentV2
struct Counter {
  @Local count: number = 0

  build() {
    Button(`${this.count}`)
      .onClick(() => {
        this.count++;
      })
  }
}

30.2 注意事项

  1. 只在 @ComponentV2 体系中使用。
  2. 本地状态的所有权属于当前组件。
  3. 不要把父组件输入复制为 @Local 后就期望继续自动同步;外部输入应使用 @Param 等机制。
  4. 复杂对象状态建议配合 @ObservedV2 / @Trace,而不是期待任意深层属性都会自动触发刷新。

31. @Param:V2 的外部输入

31.1 它做什么

@Param 表示组件从外部接收的参数,概念上接近 V1 的 @Prop,但 V2 的参数可配合不同修饰语义处理来源、一次性初始化或双向场景。

@ComponentV2
struct UserName {
  @Param name: string = '访客'

  build() {
    Text(`你好,${this.name}`)
  }
}

31.2 注意事项

  1. @Param 的核心含义是“外部输入”,不要把它当作任意可自由修改的私有状态。
  2. 组件内部需要独立状态时,用 @Local。
  3. 需要向父组件报告操作时,优先使用 @Event。
  4. @Param 与 @Once 等的组合规则要以当前 SDK 为准;不要照搬 V1 @Prop 的所有习惯。

32. @Once:一次性外部初始化

32.1 它做什么

@Once 用于表达“只在初始化时从外部接收一次”的参数语义。它适合子组件需要以外部值作为初始值,但之后自己独立维护状态的场景。
概念示例:

@ComponentV2
struct EditableTitle {
  @Param @Once initialTitle: string = ''
  @Local currentTitle: string = ''

  aboutToAppear(): void {
    this.currentTitle = this.initialTitle;
  }

  build() {
    TextInput({ text: this.currentTitle })
      .onChange((value: string) => {
        this.currentTitle = value;
      })
  }
}

32.2 注意事项

  1. 它表达的是“初始输入”而不是持续双向同步。
  2. 适合编辑草稿、初始筛选条件、一次性初始化配置等。
  3. 外部值后续变化是否影响组件,要明确设计,不要给用户造成“为什么父组件改了但子组件没变”的意外。
  4. V2 组合装饰器的顺序、限制和生命周期细节以当前 SDK 文档为准。

33. @Event:V2 子组件向外发送事件

33.1 它做什么

@Event 用于表达子组件向外通知事件,概念上接近 V1 中“把回调函数作为普通字段传入”的模式。
概念示例:

@ComponentV2
struct DeleteButton {
  @Event onDelete: () => void = () => {}

  build() {
    Button('删除')
      .onClick(() => {
        this.onDelete();
      })
  }
}

33.2 注意事项

  1. @Event 表示“发生了什么”,而不是让子组件随意掌控父组件全部状态。
  2. 事件命名应使用动作语义:onClick、onDelete、onValueChange、onSubmit。
  3. 传出必要参数,如 id、新值、操作类型;不要把整个页面对象作为参数传出。
  4. 没有外部处理函数时,提供空函数默认值,可避免调用空值。
  5. V2 的事件传递与 @Param 的结合写法,请按当前 SDK 类型提示落地。

34. @Monitor:V2 状态变化监听

34.1 它做什么

@Monitor 用于监听 V2 状态或状态路径变化,并在变化后执行指定方法。概念上接近 V1 的 @Watch,但支持更精细的状态观察能力。
概念示意:

@ComponentV2
struct FormPage {
  @Local input: string = ''

  @Monitor('input')
  onInputChanged(): void {
    console.info(`输入变化:${this.input}`);
  }

  build() {
    TextInput({ text: this.input })
      .onChange((value: string) => {
        this.input = value;
      })
  }
}

34.2 注意事项

  1. 监听路径语法、回调参数形式可能随 SDK 版本变化,必须以当前官方文档为准。
  2. 不要在监听方法内无条件写回同一个被监听状态,避免循环。
  3. 适合校验、日志、联动更新等;不适合承载重业务流程。
  4. 频繁输入、滚动或动画状态会高频触发,涉及网络操作时应做节流/防抖。
  5. 如果值完全可以通过其他值直接计算,优先使用 @Computed,不要保存重复状态。

35. @Computed:V2 计算属性

35.1 它做什么

@Computed 用于声明派生值:它由其他状态计算出来,不应该由多处手动赋值。
概念示例:

@ComponentV2
struct CartSummary {
  @Local price: number = 19.9
  @Local quantity: number = 2

  @Computed
  get totalPrice(): number {
    return this.price * this.quantity;
  }

  build() {
    Text(`总价:${this.totalPrice}`)
  }
}

35.2 为什么重要

不推荐维护重复数据:

// 容易不同步的思路
@Local price: number = 19.9
@Local quantity: number = 2
@Local totalPrice: number = 39.8

一旦改了 price 或 quantity,你可能忘记改 totalPrice。
更好的思路是:

@Computed
get totalPrice(): number {
  return this.price * this.quantity;
}

35.3 注意事项

  1. 计算属性应尽量纯粹:只根据状态计算结果,不要修改状态、发网络请求或产生不可预测副作用。
  2. 不要手动给计算属性赋值。
  3. 适合总价、已完成数量、格式化文本、是否允许提交等派生结果。
  4. 计算逻辑过重时要注意性能;必要时拆分、缓存或在业务层处理。
  5. @Computed 的精确声明形式与适用位置以当前 SDK 为准。

36. @Provider 与 @Consumer:V2 跨层级状态共享

36.1 它们做什么

它们是 V2 的跨层级提供与消费机制,概念上接近 V1 的 @Provide / @Consume。

@ComponentV2
struct Root {
  @Provider themeName: string = '浅色模式'
}
@ComponentV2
struct DeepChild {
  @Consumer themeName: string
}

36.2 注意事项

  1. V2 中使用 @Provider / @Consumer;V1 中使用 @Provide / @Consume。不要混写。
  2. 只用于存在清晰祖先/后代关系的共享状态。
  3. 不要通过跨层级共享逃避组件 API 设计;可复用组件仍应优先通过参数表达明确输入。
  4. 共享状态应有唯一、清晰的所有者,避免多个后代同时隐式修改。
  5. 当前 SDK 对类型、默认值、注入规则可能有细节限制,按 IDE 类型提示验证。

第六部分:ArkTS 并发相关装饰器

37. @Sendable:标记可跨并发上下文传递的类

37.1 它做什么

ArkTS 并发编程中,@Sendable 用于标记可安全跨并发上下文传递的类或对象类型。它服务于线程/任务隔离与数据安全,不是 UI 状态装饰器。
概念示例:

@Sendable
class WorkerTaskData {
  taskId: number;
  title: string;

  constructor(taskId: number, title: string) {
    this.taskId = taskId;
    this.title = title;
  }
}

37.2 注意事项

  1. 仅在使用 ArkTS 并发能力、需要跨并发任务传递对象时考虑。
  2. 可发送对象通常要满足并发安全限制;不要把 UI 组件实例、Context、控制器、不可序列化资源直接传入并发任务。
  3. @Sendable 不会自动让代码线程安全;共享数据、竞争条件、生命周期仍需要你设计。
  4. 初学 ArkUI 页面开发时通常不需要它。
  5. 不同 SDK 对可发送类型与限制有明确规则,使用前务必查看当前官方并发文档。

38. @Concurrent:声明并发执行的函数

38.1 它做什么

@Concurrent 用于声明可在并发环境中执行的函数。它用于耗时计算等并发场景,避免阻塞 UI。
概念示例:

@Concurrent
function calculateSum(limit: number): number {
  let sum: number = 0;

  for (let index: number = 1; index <= limit; index++) {
    sum += index;
  }

  return sum;
}

38.2 注意事项

  1. 不能把 @Concurrent 当作“所有函数加速器”。线程调度和数据传输有成本,小任务不一定更快。
  2. 并发函数不能随意访问 UI 状态、组件实例或线程不安全资源。
  3. 耗时结果回到 UI 后,要在合适的 UI 上下文中更新状态。
  4. 密集计算、批处理、图片/数据预处理更适合;单纯按钮点击逻辑通常不需要。
  5. 当前 SDK 的并发 API、调用方式和限制可能变化,按官方并发文档实现完整任务调度

第七部分:你可能会误认成装饰器的内容

39. 它们不是字段装饰器,但非常常见

在这里插入图片描述

40. @Styles、@Extend、@Builder 为什么也叫“装饰器”

从语法形式上,它们带有 @,并由 ArkUI 编译/框架识别;但从用途上:
• @State、@Prop、@Link 解决的是数据与状态流。
• @Builder、@BuilderParam 解决的是UI 片段复用与插槽。
• @Styles、@Extend 解决的是样式复用与组件扩展。
• @Entry、@Component 解决的是页面和组件身份。
学习时千万不要只背名称,要先问:它在管理“组件身份”“数据来源”“UI 复用”还是“样式”?

第八部分:最实用的选择指南

41. 我该用哪个装饰器

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

42. V1 / V2 选择建议

42.1 初学者或已有 V1 项目

优先使用:

@Component
@State
@Prop
@Link
@Provide / @Consume
@Observed / @ObjectLink
@Watch

原因:教程、现有项目示例和基础组件化实践更常见,容易建立数据流直觉。

42.2 需要更精细状态追踪的新模块

评估 V2:

@ComponentV2
@Local
@Param
@Event
@ObservedV2
@Trace
@Computed
@Monitor
@Provider / @Consumer

前提:

  1. 当前项目 SDK 已支持。
  2. 团队统一使用规范。
  3. 你理解 V1 与 V2 不能随意混用。
  4. 已验证真机/模拟器构建,而不只是在网上复制代码。

第九部分:最容易踩坑的 20 条规则

  1. 不要在 build() 中无条件修改 @State。
    这会引发重复构建、循环刷新或难以定位的 UI 问题。
  2. 不要把普通字段当成响应式状态。
    用户操作后要刷新 UI 的字段使用 @State 或相应状态装饰器。
  3. 父传子展示用 @Prop,不要让子组件偷偷改数据。
  4. 需要双向绑定才使用 @Link。
    @Link 不是比回调“更高级”的通用解法。
  5. @Link 的父组件传参要使用 $变量名。
  6. @Observed 和 @ObservedV2 不要混用。
  7. @Component 和 @ComponentV2 内的状态装饰器不要混用。
  8. 不要把 AppStorage 当永久数据库。
    应用级 UI 状态与永久数据存储是两个概念。
  9. 同一个 AppStorage 键的数据类型必须统一。
  10. @Provide / @Consume 不要滥用。
    共享过多隐式状态会让数据来源无法追踪。
  11. @Watch / @Monitor 回调中不要无条件修改同一监听状态。
  12. 派生值优先计算,不要维护重复状态。
    V2 用 @Computed;V1 可用普通计算方法或表达式。
  13. 列表用稳定业务 ID 做 ForEach 键。
    不要优先用会变化的索引。
  14. @Builder 是小块 UI 复用,@Component 是完整组件复用。
  15. @BuilderParam 的插槽逻辑不要无限嵌套。
    复杂时拆成真正组件。
  16. @Styles 和 @Extend 只负责样式/扩展,不应承担业务数据流。
  17. @CustomDialog 的关闭、确认和取消逻辑要完整。
  18. @Preview 只用于开发辅助,不能替代真机/模拟器验证。
  19. 遇到“装饰器不可用”,先查 API Level 和 SDK。
    不一定是语法错,可能是工程版本不支持。
  20. 看到网上不同写法时,先判断它属于 V1 还是 V2。
    最危险的错误就是把两套体系的装饰器混在同一个组件中。

第十部分:完整综合练习——装饰器学习面板

这个例子综合使用:

• @Entry、@Component
• @State
• @Prop
• 回调事件
• @Builder
• @BuilderParam
• @Styles
• ForEach、条件渲染

将以下代码完整替换 entry/src/main/ets/pages/Index.ets 后运行。

interface Lesson {
  id: number;
  title: string;
  decorator: string;
  done: boolean;
}

@Styles
function cardStyle() {
  .width('100%')
  .padding(16)
  .backgroundColor('#F7F8FA')
  .borderRadius(14)
}

@Component
struct LessonRow {
  @Prop lesson: Lesson = {
    id: 0,
    title: '',
    decorator: '',
    done: false
  }

  onToggle: (id: number) => void = () => {}

  build() {
    Row({ space: 12 }) {
      Column({ space: 5 }) {
        Text(this.lesson.title)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#222222')

        Text(this.lesson.decorator)
          .fontSize(14)
          .fontColor('#0A59F7')

        Text(this.lesson.done ? '已掌握' : '待学习')
          .fontSize(13)
          .fontColor(this.lesson.done ? '#2E8B57' : '#E67E22')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      Button(this.lesson.done ? '复习' : '完成')
        .fontSize(14)
        .onClick(() => {
          this.onToggle(this.lesson.id);
        })
    }
    .cardStyle()
    .alignItems(VerticalAlign.Center)
  }
}

@Component
struct SectionCard {
  @Prop title: string = ''
  @BuilderParam content: () => void = this.defaultContent

  @Builder
  private defaultContent(): void {
    Text('暂无内容')
      .fontColor('#999999')
  }

  build() {
    Column({ space: 12 }) {
      Text(this.title)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Divider()
        .color('#E5E6EB')

      this.content()
    }
    .cardStyle()
  }
}

@Entry
@Component
struct Index {
  @State lessons: Lesson[] = [
    { id: 1, title: '组件内部状态', decorator: '@State', done: true },
    { id: 2, title: '父组件传值', decorator: '@Prop', done: true },
    { id: 3, title: '双向链接', decorator: '@Link', done: false },
    { id: 4, title: '跨层级共享', decorator: '@Provide / @Consume', done: false },
    { id: 5, title: 'UI 构建函数', decorator: '@Builder', done: false }
  ]

  @Builder
  private buildSummaryRow(label: string, value: string): void {
    Row() {
      Text(label)
        .fontSize(15)
        .fontColor('#666666')
        .layoutWeight(1)

      Text(value)
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
    }
    .width('100%')
  }

  build() {
    Scroll() {
      Column({ space: 16 }) {
        Text('鸿蒙装饰器学习面板')
          .fontSize(27)
          .fontWeight(FontWeight.Bold)
          .width('100%')

        SectionCard({
          title: '学习概览',
          content: () => {
            Column({ space: 10 }) {
              this.buildSummaryRow('总知识点', `${this.lessons.length}`)
              this.buildSummaryRow('已掌握', `${this.getDoneCount()}`)
              this.buildSummaryRow('待学习', `${this.lessons.length - this.getDoneCount()}`)
            }
            .width('100%')
          }
        })

        SectionCard({
          title: '知识点列表',
          content: () => {
            Column({ space: 10 }) {
              ForEach(this.lessons, (lesson: Lesson) => {
                LessonRow({
                  lesson: lesson,
                  onToggle: (id: number) => {
                    this.toggleLesson(id);
                  }
                })
              }, (lesson: Lesson) => lesson.id.toString())
            }
            .width('100%')
          }
        })

        if (this.getDoneCount() === this.lessons.length) {
          SectionCard({
            title: '完成提示',
            content: () => {
              Text('恭喜!你已完成本页全部装饰器基础练习。下一步可尝试把 LessonRow 单独拆到 components 目录。')
                .fontSize(15)
                .lineHeight(24)
                .fontColor('#2E8B57')
                .width('100%')
            }
          })
        }
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .height('100%')
  }

  private getDoneCount(): number {
    return this.lessons.filter((lesson: Lesson) => lesson.done).length;
  }

  private toggleLesson(id: number): void {
    this.lessons = this.lessons.map((lesson: Lesson) => {
      if (lesson.id === id) {
        return {
          id: lesson.id,
          title: lesson.title,
          decorator: lesson.decorator,
          done: !lesson.done
        };
      }
      return lesson;
    });
  }
}

第十一部分:官方学习入口与版本核对

本文基于 ArkUI 的官方状态管理与组件文档体系整理。实际编码时,遇到 SDK 差异请优先核对下列官方主题:
• @State:组件内状态
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state
• AppStorage:应用全局 UI 状态
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-appstorage
• LocalStorage:页面级 UI 状态
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-localstorage
• @ComponentV2:V2 自定义组件
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-componentv2
• OpenHarmony 状态管理概述(含 V2 装饰器地图)
https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/quick-start/arkts-state-management-overview.md
最后记住一个最实用的口诀:

组件自己变:@State
父给子看:@Prop
子改父值:@Link
跨层共享:@Provide / @Consume
对象深观察:@Observed / @ObjectLink
状态变后处理:@Watch
应用级共享:@StorageLink
页面级共享:@LocalStorageLink
小 UI 复用:@Builder
可变内容插槽:@BuilderParam
样式复用:@Styles / @Extend
V2 新体系:@ComponentV2 + @Local + @Param + @Trace

当你看到一个装饰器时,先问自己:“它是在定义组件身份、管理数据来源、同步状态、复用 UI,还是复用样式?”——知道这个答案,就不容易混淆。

Logo

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

更多推荐