大家在开发 HarmonyOS 应用时,是不是经常遇到组件之间数据传递的难题?尤其是跨了好几层的组件,想实现数据双向同步简直头都大了。今天就给大家介绍一对状态管理 V2 版本的新工具 ——@Provider 和 @Consumer 装饰器,有了它们,跨组件层级的数据双向同步就变得简单多了。

一、@Provider 和 @Consumer 是啥?

简单说,@Provider 就是数据的 "提供方",@Consumer 是数据的 "消费方"。父组件里用 @Provider 声明的数据,所有子组件(不管嵌套多少层)都能用 @Consumer 绑定相同的 key 来获取,而且是双向同步的 —— 一方改了,另一方立马跟着变。

不过有几个关键点得先记牢:

  • 这俩是状态管理 V2 的装饰器,只能在 @ComponentV2 里用,用在 @Component 里会报错
  • 从 API version 12 开始才支持
  • 两者装饰的数据类型必须一致
  • 消费方会找最近的父组件里的提供方,找不到就用自己的默认值

给大家看个最简单的例子感受下:

@Entry
@ComponentV2
struct Parent {
  // 父组件提供数据
  @Provider() message: string = "我是父组件的数据";

  build() {
    Column() {
      Text("父组件: " + this.message)
      Child() // 嵌套子组件
    }
  }
}

@ComponentV2
struct Child {
  // 子组件消费数据,找不到就用"默认值"
  @Consumer() message: string = "默认值";

  build() {
    Text("子组件: " + this.message)
  }
}

这个例子里,子组件的 @Consumer 会自动找到父组件的 @Provider,所以最终会显示相同的内容。

二、和 V1 版本的 @Provide/@Consume 有啥区别?

可能有小伙伴用过 V1 版本的 @Provide 和 @Consume,虽然名字差不多,但 V2 的这对装饰器有不少新特性。咱们直接上对比:

重点区别 @Provider 和 @Consumer(V2) @Provide 和 @Consume(V1)
本地初始化 允许,找不到提供方时用本地默认值 禁止,找不到会报错
支持类型 可以装饰函数类型 不支持函数
观察能力 只观察自身赋值变化,复杂类型要配合 @Trace 观察第一层变化,复杂类型用 @Observed 和 @ObjectLink
匹配规则 只认 alias,没写 alias 就用属性名 既认 alias 也认属性名,优先 alias
重名处理 允许重名,消费方找最近的提供方 默认不允许重名,需要配置才允许

最实用的改进就是 V2 的 @Consumer 可以设置默认值了,就算找不到对应的 @Provider 也不会崩溃,这在开发复杂组件时太有用了。

举个例子,V2 的 @Consumer 可以这样写:

@ComponentV2
struct Child {
  // 找不到Provider时,就用"本地默认值"
  @Consumer() count: number = 100;

  build() {
    Text("计数: " + this.count)
  }
}

而 V1 的 @Consume 如果找不到对应的 @Provide,运行时就会报错,这一点大家一定要注意区分。

三、基本用法和规则

1. 怎么用 alias 进行匹配?

@Provider 和 @Consumer 是通过 "别名(alias)" 来匹配的,没写 alias 的话就默认用变量名当 alias。咱们看几个例子就明白了:

情况一:都不用 alias,用变量名匹配

@ComponentV2
struct Parent {
  // 没写alias,默认用变量名"username"当alias
  @Provider() username: string = "张三";

  build() {
    Child()
  }
}

@ComponentV2
struct Child {
  // 没写alias,用变量名"username"找Provider,能找到
  @Consumer() username: string = "默认名";

  build() {
    Text(this.username) // 显示"张三"
  }
}

情况二:都指定相同的 alias

@ComponentV2
struct Parent {
  // 指定alias为"user"
  @Provider("user") name: string = "李四";

  build() {
    Child()
  }
}

@ComponentV2
struct Child {
  // 用alias"user"找Provider,能找到
  @Consumer("user") myName: string = "默认名";

  build() {
    Text(this.myName) // 显示"李四"
  }
}

情况三:alias 不匹配

@ComponentV2
struct Parent {
  // alias是"title"
  @Provider("title") info: string = "重要信息";

  build() {
    Child()
  }
}

@ComponentV2
struct Child {
  // alias是"content"(变量名是info)
  @Consumer("content") info: string = "本地信息";

  build() {
    Text(this.info) // 显示"本地信息",因为没找到匹配的Provider
  }
}

记住一个原则:alias 必须完全一致才能匹配,不管变量名是什么。

2. 变量初始化和传递规则

这对装饰器的变量初始化有两个重要规则:

  • 只能在本地初始化,不能从父组件通过参数传入
  • 可以用它们来初始化子组件的 @Param 变量

举个正确的例子:

@ComponentV2
struct Parent {
  // 必须本地初始化,不能从外面传
  @Provider() age: number = 20;

  build() {
    // 可以用age初始化子组件的@Param
    Child({ childAge: this.age })
  }
}

@ComponentV2
struct Child {
  @Param childAge: number = 0; // 接收父组件传来的值

  build() {
    Text("年龄: " + this.childAge) // 显示20
  }
}

错误的写法是试图从外部给 @Provider 变量传值,比如这样:

// 错误示例!不要这么写!
@ComponentV2
struct Parent {
  @Provider() score: number = 0; // 必须本地初始化

  build() {
    // 这样写没问题,因为是给子组件的@Param传值
    Child({ score: this.score })
  }
}

// 错误示例!@Provider不能通过参数初始化!
@ComponentV2
struct BadExample {
  // 下面这行代码会报错!
  @Provider() score: number; // 没有本地初始化

  build() {
    Text("分数: " + this.score)
  }
}

3. 使用限制要记牢

使用这对装饰器时有几个硬性规定,违反了会编译报错:

  1. 只能在 @ComponentV2 装饰的组件里用,不能在 @Component(V1 组件)里用
  2. 只能装饰组件里的成员变量,不能装饰 class 的属性
  3. 必须本地初始化,不能从父组件通过构造参数传入
  4. 装饰的变量类型必须一致,比如 @Provider 是 number,@Consumer 也得是 number

这些限制都是为了保证组件的稳定性,大家开发时多注意就行。

四、实战场景用法

1. 双向同步数据

这是最常用的场景,父组件和子组件的变量能实时同步,一方修改,另一方马上更新。

@Entry
@ComponentV2
struct Parent {
  @Provider() text: string = "初始值";

  build() {
    Column({ space: 20 }) {
      Text("父组件: " + this.text)
      Button("父组件添加字符")
        .onClick(() => {
          this.text += "P"; // 修改父组件的变量
        })
      Child() // 嵌套子组件
    }
  }
}

@ComponentV2
struct Child {
  @Consumer() text: string = "子组件默认值";

  build() {
    Column({ space: 20 }) {
      Text("子组件: " + this.text)
      Button("子组件添加字符")
        .onClick(() => {
          this.text += "C"; // 修改子组件的变量
        })
    }
  }
}

运行这个例子,不管点击父组件还是子组件的按钮,两边的文本都会同时更新。因为 @Provider 和 @Consumer 建立了双向绑定,一方变化会自动同步给另一方。

如果 alias 不匹配,就建立不了双向绑定,大家可以试试把上面例子里的变量名改成不一样的,比如父组件用 text1,子组件用 text,这时两边的修改就互不影响了。

2. 传递回调事件

除了传递数据,还能传递函数,这在子组件需要通知父组件做某事时特别方便。比如子组件触发了点击事件,要让父组件处理。

@Entry
@ComponentV2
struct Parent {
  message: string = "等待消息...";

  // 定义一个回调函数,用@Provider装饰
  @Provider() onMessage: (msg: string) => void = (msg: string) => {
    this.message = "收到: " + msg;
  };

  build() {
    Column({ space: 20 }) {
      Text(this.message)
      Child() // 子组件会用到这个回调
    }
  }
}

@ComponentV2
struct Child {
  // 接收父组件的回调函数,默认是个空函数
  @Consumer() onMessage: (msg: string) => void = () => {};

  build() {
    Button("发送消息给父组件")
      .onClick(() => {
        // 调用回调函数,通知父组件
        this.onMessage("你好,父组件!");
      })
  }
}

这个例子里,子组件通过 @Consumer 拿到父组件的 onMessage 函数,点击按钮时调用这个函数,父组件就能收到消息并更新 UI 了。这比通过 @Param 一层一层传递函数简洁多了。

3. 处理复杂类型数据

当需要传递对象、数组这类复杂类型时,单纯用 @Provider 和 @Consumer 只能观察到整个变量被替换的情况,要观察内部属性的变化,得配合 @Trace 装饰器。

// 定义一个用户类,属性用@Trace装饰
@ObservedV2
class User {
  @Trace name: string; // 用@Trace让属性变化可被观察
  @Trace age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

@Entry
@ComponentV2
struct Parent {
  // 提供一个User数组
  @Provider('userList') users: User[] = [
    new User('小明', 18),
    new User('小红', 20)
  ];

  build() {
    Column({ space: 10 }) {
      Button("添加用户")
        .onClick(() => {
          this.users.push(new User('新用户', 25));
        })
      Button("小明年龄+1")
        .onClick(() => {
          this.users[0].age++;
        })
      Child()
    }
  }
}

@ComponentV2
struct Child {
  // 消费用户列表,默认是空数组
  @Consumer('userList') users: User[] = [];

  build() {
    Column() {
      ForEach(this.users, (user: User) => {
        Column() {
          Text("姓名: " + user.name)
          Text("年龄: " + user.age)
          Divider()
        }
      })
    }
  }
}

这里有两个关键点:

  • 自定义类要用 @ObservedV2 装饰
  • 类里的属性要用 @Trace 装饰,这样属性变化时才能被观察到

这样处理后,不管是添加新用户(修改数组),还是修改某个用户的年龄(修改对象属性),子组件都能实时更新 UI。

4. 处理重名的 @Provider

当组件树里有多个同名的 @Provider 时,@Consumer 会找到最近的那个,这种特性适合开发有层级关系的组件。

@Entry
@ComponentV2
struct Grandparent {
  @Provider() level: number = 1; // 最上层的Provider

  build() {
    Column() {
      Text("祖父组件 level: " + this.level)
      Parent() // 嵌套父组件
    }
  }
}

@ComponentV2
struct Parent {
  @Provider() level: number = 2; // 中间层的Provider
  @Consumer() levelFromGrandpa: number = 0; // 找祖父组件的Provider

  build() {
    Column() {
      Text("父组件 从祖父那拿到的level: " + this.levelFromGrandpa) // 显示1
      Text("父组件自己的level: " + this.level) // 显示2
      Child() // 嵌套子组件
    }
  }
}

@ComponentV2
struct Child {
  @Consumer() level: number = 0; // 找最近的Parent的Provider

  build() {
    Text("子组件拿到的level: " + this.level) // 显示2
  }
}

运行这个例子会发现:

  • Parent 里的 @Consumer 找到的是 Grandparent 的 @Provider(值为 1)
  • Child 里的 @Consumer 找到的是 Parent 的 @Provider(值为 2)

这种 "就近原则" 在开发嵌套组件时很有用,比如多级菜单、树形控件等场景。

5. 初始化 @Param 变量

@Provider 和 @Consumer 还能用来初始化子组件的 @Param 变量,实现多层组件的数据传递。

@Entry
@ComponentV2
struct Grandparent {
  @Provider() count: number = 10; // 最上层的数据源

  build() {
    Column() {
      Text("祖父组件 count: " + this.count)
        .onClick(() => {
          this.count++; // 点击增加计数
        })
      Parent()
    }
  }
}

@ComponentV2
struct Parent {
  @Consumer() count: number = 0; // 接收祖父组件的count

  build() {
    Column() {
      Text("父组件 count: " + this.count)
        .onClick(() => {
          this.count++; // 点击也能增加计数
        })
      // 用count初始化子组件的@Param
      Child({ childCount: this.count })
    }
  }
}

@ComponentV2
struct Child {
  @Param childCount: number = 0; // 接收父组件传来的值

  build() {
    Text("子组件 count: " + this.childCount)
  }
}

这个例子里,点击祖父组件或父组件的文本,count 值会变化,并且会同步到所有关联的组件:

  • 祖父的 @Provider 变化 → 父的 @Consumer 变化 → 子的 @Param 变化
  • 父的 @Consumer 变化 → 祖父的 @Provider 变化 → 子的 @Param 变化

形成了一个完整的双向传递链,这在开发表单类组件时非常实用。

五、使用建议和注意事项

虽然 @Provider 和 @Consumer 很好用,但也不能滥用,给大家几个建议:

  1. 减少使用频率:它们会让组件之间产生强依赖,父组件变化可能会影响子组件的行为,不利于组件复用。能用水银(@State+@Prop)或参数传递解决的,就别用这对装饰器。

  2. 注意组件层级:@Consumer 的值取决于它所在的组件层级,同一个子组件放在不同的父组件下,可能会得到不同的值,这是正常现象,但开发时要考虑到。

  3. 复杂场景优先用状态管理:如果组件嵌套超过 3 层,或者需要跨页面传递数据,建议用 AppStorage、LocalStorage 这些全局状态管理工具,而不是多层级的 @Provider 和 @Consumer。

  4. 给 @Consumer 设默认值:养成给 @Consumer 设置合理默认值的习惯,这样就算找不到对应的 @Provider,组件也能正常显示,方便单独调试子组件。

总结

@Provider 和 @Consumer 作为 HarmonyOS 状态管理 V2 的重要装饰器,解决了跨组件层级数据双向同步的问题,相比 V1 版本更加灵活和稳定。它们通过 alias 匹配,支持双向同步,允许设置默认值,还能传递函数,这些特性让组件通信变得简单。

不过大家也要注意它们的使用限制和组件依赖问题,合理使用才能发挥最大作用。希望这篇文章能帮大家掌握这对装饰器的用法,开发出更优雅的 HarmonyOS 应用!

Logo

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

更多推荐