HarmonyOS 鸿蒙 ArkTS/ArkUI 装饰器大全
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 注意事项
- @Entry 不等于“任意可复用组件”。它强调页面入口。
- 普通子组件通常只需要 @Component,不要都加 @Entry。
- 页面配置、路由配置与页面文件之间需要匹配;如果路由不到页面,优先检查工程配置和页面注册。
- 可通过 @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 注意事项
- @Component 是 V1 状态管理体系 的核心组件装饰器。
- 组件字段若需要驱动 UI 更新,需要使用相应状态装饰器,例如 @State;普通字段不具备响应式刷新能力。
- 组件名用大驼峰命名,例如 CourseCard、ProfileHeader。
- build() 主要负责描述 UI;不要在 build() 中无条件修改状态。
- 一个组件应有清楚职责:例如“课程卡片”“数量选择器”,不要使用无意义的 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 注意事项
- @Preview 的具体能力与 IDE、SDK、工程配置密切相关;若当前 IDE 不显示预览入口,应以代码运行结果为准。
- 预览数据应尽量使用固定的模拟数据,不依赖网络、数据库、权限或复杂页面路由。
- @Preview 是开发辅助,不是用户运行时页面入口;不能替代 @Entry。
- 不同版本可能支持不同的预览参数,优先使用 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 注意事项
- 不要把 @Reusable 当成普通组件的必写项;只有明确存在复用/性能诉求时才使用。
- 可复用组件的状态设计要更谨慎:不要假设组件每次出现都一定是全新实例。
- 避免把一次性业务流程、复杂页面级状态塞到可复用列表项内部。
- 是否可用及具体复用规则以当前 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 注意事项
- @CustomDialog 修饰的是弹窗内容结构,不是普通页面。
- 弹窗通常通过 CustomDialogController 打开和关闭。
- 使用可选链 this.controller?.close() 可避免控制器尚未注入时直接报错。
- 不要在弹窗中长期持有页面或 Context 的不必要引用,避免生命周期混乱。
- 对删除、退出、清空等高风险操作,弹窗应清楚说明后果,并提供“取消”入口。
- 部分 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 注意事项
- @State 只适合“当前组件拥有”的状态。
- 在子组件中直接修改来自父组件的数据,不应靠复制一份 @State 来“假装同步”;应根据场景选 @Prop、@Link 或回调。
- 对对象、数组的深层变更要尤其注意响应式规则。简单入门中,推荐生成新对象/新数组后重新赋值:
this.todos = [...this.todos, newTodo];
this.todos = this.todos.map((item: Todo) => {
return item.id === id ? { ...item, done: !item.done } : item;
});
- 不要在 build() 内无条件执行 this.count++、网络请求、修改 @State。这可能导致重复构建或循环刷新。
- @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 注意事项
- 把 @Prop 当作“父组件给子组件的输入”,而不是子组件的私有可变状态。
- 基本类型、对象、数组均可传递;但复杂对象的变更规则与 SDK 版本有关,展示型子组件应尽量保持只读。
- 子组件需要通知父组件发生操作时,优先传回调函数:
onDelete: (id: number) => void = () => {} - 不要依赖子组件修改 @Prop 来更新父组件。需要双向绑定时用 @Link,或使用“回调 + 父组件更新 @State”。
- 给 @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 注意事项
- 父组件传给 @Link 时,必须传状态变量的链接形式,例如 $count。
- @Link 适用于真正的双向编辑,例如数量选择器、开关、编辑表单。
- 不要用 @Link 代替所有回调。数据可被任意子组件直接修改,会让大型项目的数据流难追踪。
- 列表项的“删除”“跳转详情”等一次性事件,通常更推荐回调;简单字段编辑才更适合 @Link。
- 子组件没有有效 @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 注意事项
- @Provide / @Consume 解决“跨层级传值”,不是全局状态管理的万能替代品。
- 提供者与消费者必须处在同一组件树的祖先/后代关系中。
- 多个同名提供者存在时,离消费者更近的提供者通常会产生遮蔽效果;命名要清楚。
- 共享数据过多会让来源不透明。跨层级共享少量主题、用户偏好、上下文信息较合适。
- 用于复杂业务共享状态时,要明确谁拥有和修改数据,避免多个消费者随意写入造成调试困难。
- 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 注意事项
- @Observed 修饰的是类,不是 struct 组件字段。
- 它属于 V1 深层对象响应式机制,常与 @ObjectLink 配套。
- 不要为了所有普通数据都定义 class + @Observed;简单数据可用接口和不可变更新方式。
- 复杂嵌套对象、数组元素响应式容易增加理解成本。初学项目可优先采用“新对象/新数组重新赋值”的方式。
- 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 注意事项
- @ObjectLink 的对象类型应是 @Observed 类实例。
- 它适合子组件需要读写某个对象属性的场景。
- 不要把普通 interface 对象直接当作 @ObjectLink 的对象使用。
- 使用对象引用传递时,组件之间耦合更强。对于“子组件只展示数据”的场景,仍优先使用 @Prop。
- 多层嵌套与大量对象联动时,建议评估是否迁移到 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 注意事项
- @Watch(‘方法名’) 中是方法名字符串,方法需要定义在同一组件内。
- 监听方法中再次修改被监听的同一状态,可能形成重复触发或逻辑循环;必须设计清楚终止条件。
- @Watch 适合执行“状态变化后的附加动作”,例如输入校验、记录日志、触发派生更新。
- 如果一个结果可直接由其他状态计算得到,优先在 build() 的表达式或专门计算方法中得到,而不是滥用 @Watch 保存重复状态。
- 不要在 @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 注意事项
- 键名必须统一管理,例如 ‘globalUserName’,避免不同模块拼错键名。
- @StorageProp 是“存储到组件”的单向输入;若需要通过字段直接写回存储,使用 @StorageLink。
- 字段默认值只在键不存在或初始场景中起作用;不要误以为它能强制覆盖已有存储值。
- AppStorage 是运行时 UI 状态容器,不等同于永久持久化。
- 不要把密码、令牌、身份证号等敏感数据直接作为普通 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 注意事项
- @StorageLink 是双向的,写入影响范围比 @State 大;不要用它保存仅属于一个小组件的临时状态。
- 多个组件连接同一个键时,任何一处修改都会影响全部连接组件。要有明确所有权和命名规范。
- AppStorage 中同一个键的数据类型要保持一致。不要某处按 number 使用、另一处按 string 使用。
- @StorageLink 不等于自动永久保存。应用重启后的行为取决于是否配置了持久化能力。
- 页面销毁后不再需要的临时键,可根据业务设计清理,避免全局状态无限堆积。
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 注意事项
- 它只适合页面作用域的数据共享,不是应用全局状态。
- LocalStorage 实例必须被正确传入页面入口/组件树,否则字段找不到预期数据。
- 单向读取场景用 @LocalStorageProp;需要双向写回用 @LocalStorageLink。
- 不同页面的 LocalStorage 实例相互独立,除非你显式传递同一个实例。
- 由于入口参数细节容易受 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 注意事项
- 使用前要保证该组件处在携带对应 LocalStorage 的页面组件树中。
- 它是双向链接;多个组件使用同一键时,输入会实时同步。
- 本页面短暂搜索词、筛选条件、标签选择等场景很合适。
- 页面离开后是否保留由 LocalStorage 实例生命周期决定,不要把它误认为可靠的长期存储。
- 当状态只在一个组件内部存在时,直接使用 @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 注意事项
- @Builder 不是一个完整独立组件,通常在所属组件内部调用。
- 组件之间需要复用、需要独立状态或需要独立文件时,更适合 @Component。
- @Builder 适合 UI 片段,不适合把复杂业务逻辑隐藏得过深。
- 参数类型应明确,尤其是对象参数。
- 如果构建函数需要访问所属组件状态,注意 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 注意事项
- @BuilderParam 适合“固定外壳 + 可变内容”,例如通用卡片、弹窗、列表项容器。
- 给它提供默认 @Builder,可让组件在未传内容时有合理显示。
- 传入函数时不要立刻执行:
// 正确
content: () => {
Text('内容')
}
// 不要写成“执行结果”
- 如果插槽内部需要大量状态和逻辑,考虑把它拆成独立 @Component,避免嵌套过深。
- 父组件传入的 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 注意事项
- @Styles 适合没有复杂参数、没有强业务语义的通用外观。
- 它不是替代 @Component 的方案。需要封装结构、交互和状态时,请使用组件。
- 样式函数能调用哪些属性取决于应用目标组件;不要在样式中写某些组件不支持的属性。
- 全局样式名称要避免冲突,建议按用途命名,如 cardStyle、pageTitleStyle。
- 样式过大、包含太多条件逻辑时,代码反而难读;保持单一职责。
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 注意事项
- @Extend(Text) 的函数只能用于 Text,不能随意套给 Button 或 Column。
- 扩展名应体现用途而不是颜色,例如 primaryButtonStyle() 比 blueButton() 更稳健。
- 不要在 @Extend 中塞进严重依赖页面业务状态的逻辑。
- 如果团队里多个模块定义同名扩展,应做好命名空间和文件组织,避免冲突。
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 注意事项
- 此能力对 SDK 版本、属性是否支持插值动画有要求;如遇编译问题,先检查当前 API 文档和 IDE 类型提示。
- 动画内应改变可观察状态,例如 @State active。
- 不要为每一个微小样式都创建动画扩展;动画应服务于反馈、层级和过渡,而不是干扰用户。
- 动画时长、曲线、可访问性偏好都应考虑;避免过长、频繁、突兀的动画。
- 复杂动画应优先使用平台推荐动画 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 注意事项
- @ComponentV2 内应使用 V2 对应状态装饰器,例如 @Local、@Param、@Event 等。
- 不要在同一个组件中把 V1 的 @State、@Prop 与 V2 的状态装饰器混写。
- 作为初学者,如果项目现有代码都使用 @Component 与 @State,先沿用 V1;没有明确需求不要强行迁移。
- V2 在不同 SDK 版本中的稳定性与支持范围可能不同。遇到“装饰器不可用”并不一定是写错,可能是工程 API 版本不匹配。
28. @ObservedV2:声明 V2 可观察类
28.1 它做什么
@ObservedV2 修饰类,表示该类参与 V2 的可观察状态机制。
@ObservedV2
class UserInfo {
@Trace name: string = '小明'
@Trace level: number = 1
}
28.2 注意事项
- @ObservedV2 修饰类。
- 它通常与 @Trace 一起使用;仅仅把类标记为可观察,未必意味着每个字段都自动按你预期追踪。
- V2 强调属性级追踪:只标记真正会影响 UI 的字段,避免无意义地把大量字段都变成响应式。
- 不要同时用 @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 注意事项
- @Trace 主要用于 @ObservedV2 类的字段。
- 不要误以为它是日志工具;它的“追踪”指 UI 状态依赖追踪。
- UI 依赖哪些字段,就追踪哪些字段;这是 V2 性能与可维护性优势的一部分。
- 嵌套对象、数组、Map/Set 等复杂数据结构的追踪粒度和限制应查看当前 SDK 官方说明。
- 不要用 @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 注意事项
- 只在 @ComponentV2 体系中使用。
- 本地状态的所有权属于当前组件。
- 不要把父组件输入复制为 @Local 后就期望继续自动同步;外部输入应使用 @Param 等机制。
- 复杂对象状态建议配合 @ObservedV2 / @Trace,而不是期待任意深层属性都会自动触发刷新。
31. @Param:V2 的外部输入
31.1 它做什么
@Param 表示组件从外部接收的参数,概念上接近 V1 的 @Prop,但 V2 的参数可配合不同修饰语义处理来源、一次性初始化或双向场景。
@ComponentV2
struct UserName {
@Param name: string = '访客'
build() {
Text(`你好,${this.name}`)
}
}
31.2 注意事项
- @Param 的核心含义是“外部输入”,不要把它当作任意可自由修改的私有状态。
- 组件内部需要独立状态时,用 @Local。
- 需要向父组件报告操作时,优先使用 @Event。
- @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 注意事项
- 它表达的是“初始输入”而不是持续双向同步。
- 适合编辑草稿、初始筛选条件、一次性初始化配置等。
- 外部值后续变化是否影响组件,要明确设计,不要给用户造成“为什么父组件改了但子组件没变”的意外。
- V2 组合装饰器的顺序、限制和生命周期细节以当前 SDK 文档为准。
33. @Event:V2 子组件向外发送事件
33.1 它做什么
@Event 用于表达子组件向外通知事件,概念上接近 V1 中“把回调函数作为普通字段传入”的模式。
概念示例:
@ComponentV2
struct DeleteButton {
@Event onDelete: () => void = () => {}
build() {
Button('删除')
.onClick(() => {
this.onDelete();
})
}
}
33.2 注意事项
- @Event 表示“发生了什么”,而不是让子组件随意掌控父组件全部状态。
- 事件命名应使用动作语义:onClick、onDelete、onValueChange、onSubmit。
- 传出必要参数,如 id、新值、操作类型;不要把整个页面对象作为参数传出。
- 没有外部处理函数时,提供空函数默认值,可避免调用空值。
- 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 注意事项
- 监听路径语法、回调参数形式可能随 SDK 版本变化,必须以当前官方文档为准。
- 不要在监听方法内无条件写回同一个被监听状态,避免循环。
- 适合校验、日志、联动更新等;不适合承载重业务流程。
- 频繁输入、滚动或动画状态会高频触发,涉及网络操作时应做节流/防抖。
- 如果值完全可以通过其他值直接计算,优先使用 @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 注意事项
- 计算属性应尽量纯粹:只根据状态计算结果,不要修改状态、发网络请求或产生不可预测副作用。
- 不要手动给计算属性赋值。
- 适合总价、已完成数量、格式化文本、是否允许提交等派生结果。
- 计算逻辑过重时要注意性能;必要时拆分、缓存或在业务层处理。
- @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 注意事项
- V2 中使用 @Provider / @Consumer;V1 中使用 @Provide / @Consume。不要混写。
- 只用于存在清晰祖先/后代关系的共享状态。
- 不要通过跨层级共享逃避组件 API 设计;可复用组件仍应优先通过参数表达明确输入。
- 共享状态应有唯一、清晰的所有者,避免多个后代同时隐式修改。
- 当前 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 注意事项
- 仅在使用 ArkTS 并发能力、需要跨并发任务传递对象时考虑。
- 可发送对象通常要满足并发安全限制;不要把 UI 组件实例、Context、控制器、不可序列化资源直接传入并发任务。
- @Sendable 不会自动让代码线程安全;共享数据、竞争条件、生命周期仍需要你设计。
- 初学 ArkUI 页面开发时通常不需要它。
- 不同 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 注意事项
- 不能把 @Concurrent 当作“所有函数加速器”。线程调度和数据传输有成本,小任务不一定更快。
- 并发函数不能随意访问 UI 状态、组件实例或线程不安全资源。
- 耗时结果回到 UI 后,要在合适的 UI 上下文中更新状态。
- 密集计算、批处理、图片/数据预处理更适合;单纯按钮点击逻辑通常不需要。
- 当前 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
前提:
- 当前项目 SDK 已支持。
- 团队统一使用规范。
- 你理解 V1 与 V2 不能随意混用。
- 已验证真机/模拟器构建,而不只是在网上复制代码。
第九部分:最容易踩坑的 20 条规则
- 不要在 build() 中无条件修改 @State。
这会引发重复构建、循环刷新或难以定位的 UI 问题。 - 不要把普通字段当成响应式状态。
用户操作后要刷新 UI 的字段使用 @State 或相应状态装饰器。 - 父传子展示用 @Prop,不要让子组件偷偷改数据。
- 需要双向绑定才使用 @Link。
@Link 不是比回调“更高级”的通用解法。 - @Link 的父组件传参要使用 $变量名。
- @Observed 和 @ObservedV2 不要混用。
- @Component 和 @ComponentV2 内的状态装饰器不要混用。
- 不要把 AppStorage 当永久数据库。
应用级 UI 状态与永久数据存储是两个概念。 - 同一个 AppStorage 键的数据类型必须统一。
- @Provide / @Consume 不要滥用。
共享过多隐式状态会让数据来源无法追踪。 - @Watch / @Monitor 回调中不要无条件修改同一监听状态。
- 派生值优先计算,不要维护重复状态。
V2 用 @Computed;V1 可用普通计算方法或表达式。 - 列表用稳定业务 ID 做 ForEach 键。
不要优先用会变化的索引。 - @Builder 是小块 UI 复用,@Component 是完整组件复用。
- @BuilderParam 的插槽逻辑不要无限嵌套。
复杂时拆成真正组件。 - @Styles 和 @Extend 只负责样式/扩展,不应承担业务数据流。
- @CustomDialog 的关闭、确认和取消逻辑要完整。
- @Preview 只用于开发辅助,不能替代真机/模拟器验证。
- 遇到“装饰器不可用”,先查 API Level 和 SDK。
不一定是语法错,可能是工程版本不支持。 - 看到网上不同写法时,先判断它属于 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,还是复用样式?”——知道这个答案,就不容易混淆。
更多推荐



所有评论(0)