HarmonyOS一杯冰美式的时间 -- @Env

作者:猫猫头啊日期:2026/1/8

一、前言

该系列依旧会带着大家,了解,开阔一些不怎么热门的API,也可能是偷偷被更新的API,也可以是好玩的,藏在官方文档的边边角角~当然也会有一些API,之前是我们辛辛苦苦的手撸代码,现在有一个API能帮我们快速实现的,希望大家能找宝藏。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

二、@Env的诞生背景

OK,步入正题把,在多设备开发的场景中,我们经常需要根据不同的设备环境(比如窗口大小、横竖屏等)来调整UI布局。以前我们可能要用Environment来获取这些信息,但Environment有个问题:它没有响应式能力,系统环境变量变化时不会自动通知组件刷新。这就导致我们需要手动监听变化,写很多重复的代码。虽然,虽然啊,他能存到AppStorage里面去,但是去监听一堆的环境变量的变化,然后设置到AppStorage里面去很不优雅啊。

好在API 22引入了@Env装饰器(有点晚了),它不仅能读取系统环境变量,还能在环境变量变化时自动触发组件刷新,爽!

1. Environment的局限性

Environment是ArkUI框架提供的设备环境查询能力,它可以将系统环境变量存入AppStorage,让我们通过@StorageProp来访问。但是Environment有个致命的缺点:没有响应式能力

啥意思呢?就是说当系统环境变量变化的时候(比如横竖屏切换),Environment不会自动通知组件刷新。我们需要手动监听变化(重点在这),然后手动更新UI。这就导致代码变得复杂,而且容易出错。

2. @Env的解决方案

API 22引入的@Env装饰器就是为了解决这个问题。它是一个响应式系统环境变量装饰器,具有以下特点:

  • 自动响应:系统环境变量变化时,自动通知@Env装饰的变量更新
  • 自动刷新@Env关联的组件会自动刷新,无需手动管理
  • 简化代码:减少了大量重复的适配逻辑 (PS:我最后还会重复这一段) 简单来说,@Env让环境变量的使用变得像状态变量一样简单,你只需要声明一个@Env变量,框架会自动帮你处理响应式更新。

三、@Env基础概念

下面两条有一条是好消息:

  • 从API version 22开始,@Env支持在@Component@ComponentV2中使用(能在V1使用哦)
  • 从API version 22开始,该装饰器支持在元服务中使(高贵22)

(1)读取环境变量

  • 目前仅支持SystemProperties.BREAK_POINT
  • 目前仅支持SystemProperties.BREAK_POINT
  • 目前仅支持SystemProperties.BREAK_POINT

重要的事情说三遍!!!!!!

@Env可以根据入参读取相应的环境变量信息。目前仅支持SystemProperties.BREAK_POINT,用于获取窗口不同宽高阈值下对应的断点值信息。

1import { uiObserver } from '@kit.ArkUI';
2
3@Entry
4@Component
5struct Index {
6  // 使用@Env装饰器获取窗口断点信息
7  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
8
9  build() {
10    Column() {
11      // 可以直接使用breakpoint获取宽度和高度断点
12      Text(`宽度断点: ${this.breakpoint.widthBreakpoint}`)
13      Text(`高度断点: ${this.breakpoint.heightBreakpoint}`)
14    }
15  }
16}
17

(2)响应式更新

当系统环境变量改变时(比如横竖屏切换、窗口大小调整),@Env会自动:

  • 通知@Env装饰变量的更新
  • 触发@Env关联组件的刷新
  • 实现界面内容的同步更新

不需要手动监听变化,框架会自动处理。

(3)可观察对象

@Env返回的对象实际上是由@ObservedV2装饰的可观察对象,其属性由@Trace装饰。这意味着:

  • 你可以使用addMonitor来监听属性的变化
  • 属性的变化会自动触发UI更新
  • 支持细粒度的响应式更新

五、@Env基本使用

1. 在@ComponentV2中使用@Env

@ComponentV2中使用@Env非常简单,我们来看一个完整的例子:

1import { uiObserver, UIUtils, window } from '@kit.ArkUI';
2import { common } from '@kit.AbilityKit';
3
4@Entry
5@ComponentV2
6struct Index {
7  // 声明@Env变量,获取窗口断点信息
8  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
9
10  // 切换横竖屏的方法
11  private changeOrientation(isLandscape: boolean) {
12    const context = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
13    window.getLastWindow(context).then((lastWindow) => {
14      // 设置窗口方向
15      lastWindow.setPreferredOrientation(
16        isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT
17      );
18    });
19  }
20
21  // 监听断点变化的回调
22  orientationChange(mon: IMonitor) {
23    mon.dirty.forEach((path: string) => {
24      console.info(`${path} changes from ${mon.value(path)?.before} to ${mon.value(path)?.now}`);
25    })
26  }
27
28  aboutToAppear(): void {
29    // @Env返回的对象实际上是@ObservedV2装饰的对象(其属性是@Trace装饰的)
30    // 所以其属性的改变可以通过addMonitor监听
31    UIUtils.addMonitor(
32      this.breakpoint,
33      ['widthBreakpoint', 'heightBreakpoint'],
34      this.orientationChange
35    );
36  }
37
38  build() {
39    Column() {
40      // 显示当前断点信息
41      Text(`Index breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
42      Text(`Index breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
43
44      // 横屏按钮
45      Button('Landscape').onClick(() => {
46        this.changeOrientation(true);
47      })
48
49      // 竖屏按钮
50      Button('Portrait').onClick(() => {
51        this.changeOrientation(false);
52      })
53
54      // 将@Env变量传递给子组件
55      CompV2({ breakpoint: this.breakpoint })
56      Comp({ breakpoint: this.breakpoint })
57    }
58  }
59}
60
61// ComponentV2子组件
62@ComponentV2
63struct CompV2 {
64  // @Env装饰的变量只能用于初始化@Param装饰的变量
65  @Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
66
67  build() {
68    Column() {
69      Text(`CompV2 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
70      Text(`CompV2 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
71    }
72  }
73}
74
75// Component子组件
76@Component
77struct Comp {
78  // @Env装饰的变量只能用于初始化常规变量
79  @Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
80
81  build() {
82    Column() {
83      Text(`Comp breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
84      Text(`Comp breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
85    }
86  }
87}
88

2. 在@Component中使用@Env

@Env@Component中的使用方式和@ComponentV2类似,我们看看代码:

1import { uiObserver, UIUtils, window } from '@kit.ArkUI';
2import { common } from '@kit.AbilityKit';
3
4@Entry
5@Component
6struct Index {
7  // 在@Component中也可以使用@Env
8  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
9
10  private changeOrientation(isLandscape: boolean) {
11    const context = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
12    window.getLastWindow(context).then((lastWindow) => {
13      lastWindow.setPreferredOrientation(
14        isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT
15      );
16    });
17  }
18
19  orientationChange(mon: IMonitor) {
20    mon.dirty.forEach((path: string) => {
21      console.info(`${path} changes from ${mon.value(path)?.before} to ${mon.value(path)?.now}`);
22    })
23  }
24
25  aboutToAppear(): void {
26    // 同样可以使用addMonitor监听
27    UIUtils.addMonitor(
28      this.breakpoint,
29      ['widthBreakpoint', 'heightBreakpoint'],
30      this.orientationChange
31    );
32  }
33
34  build() {
35    Column() {
36      Text(`Index breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
37      Text(`Index breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
38
39      Button('Landscape').onClick(() => {
40        this.changeOrientation(true);
41      })
42
43      Button('Portrait').onClick(() => {
44        this.changeOrientation(false);
45      })
46
47      CompV2({ breakpoint: this.breakpoint })
48      Comp({ breakpoint: this.breakpoint })
49    }
50  }
51}
52
53// 子组件使用方式相同
54@ComponentV2
55struct CompV2 {
56  @Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
57
58  build() {
59    Column() {
60      Text(`CompV2 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
61      Text(`CompV2 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
62    }
63  }
64}
65
66@Component
67struct Comp {
68  @Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
69
70  build() {
71    Column() {
72      Text(`Comp breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
73      Text(`Comp breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
74    }
75  }
76}
77

可以看到,@Component@ComponentV2中使用@Env的方式基本一致,主要区别在于子组件接收参数的方式。

3. 变量传递规则

@Env装饰的变量在组件间传递有严格的规则,我们需要特别注意:

(1)传递给ComponentV2子组件

@Env装饰的变量只能用于初始化@ComponentV2@Param装饰的变量:

1@Entry
2@Component
3struct Index {
4  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
5
6  build() {
7    Column() {
8      //  正确:传递给@Param
9      CompV2({ breakpoint: this.breakpoint })
10
11      //  错误:不能传递给非@Param变量
12      CompV2Invalid({ breakpoint: this.breakpoint })
13    }
14  }
15}
16
17@ComponentV2
18struct CompV2 {
19  //  正确:使用@Require @Param接收
20  @Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
21
22  build() {
23    // ...
24  }
25}
26
27@ComponentV2
28struct CompV2Invalid {
29  //  错误:缺少@Param
30  @Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
31
32  build() {
33    // ...
34  }
35}
36

(2)传递给Component子组件

@Env装饰的变量只能用于初始化@Component中的常规变量:

1@Entry
2@Component
3struct Index {
4  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
5
6  build() {
7    Column() {
8      //  正确:传递给常规变量
9      Comp({ breakpoint: this.breakpoint })
10
11      //  错误:不能传递给@ObjectLink等
12      CompInvalid({ breakpoint: this.breakpoint })
13    }
14  }
15}
16
17@Component
18struct Comp {
19  //  正确:使用@Require接收常规变量
20  @Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
21
22  build() {
23    // ...
24  }
25}
26
27@Component
28struct CompInvalid {
29  //  错误:不能使用@ObjectLink
30  @ObjectLink breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
31
32  build() {
33    // ...
34  }
35}
36

重要提示:通过BuilderNode切换窗口时,会导致@Env依据新的窗口更新环境变量实例。在切换窗口的场景中,不建议使用@Env变量来初始化子组件的常规变量,否则会造成该常规变量无法被@Env通知触发其关联UI组件刷新。具体解决方案我们会在高级场景中介绍。

六、@Env初始化流程详解

@Env变量不允许开发者初始化,其值由框架根据当前窗口的环境变量自动提供。@Env变量在被第一次读值的时候,会触发初始化。初始化遵循以下流程:

1. 从父组件中查找已有实例

框架会向上递归查找父组件:

  • 如果某个父组件在同一窗口中已经初始化过相同key的@Env变量,则直接复用该实例
  • 若未找到,则继续向上查找,直到父组件为空
  • 注意:向上查找父组件的流程会被BuilderNode打断

2. 查找当前窗口的@Env实例

如果在父组件中未找到对应的实例,则检查当前窗口是否已有相同key的@Env变量实例:

  • 如存在,则复用该窗口内的@Env实例

3. 首次请求:创建新环境变量实例

若以上两步都无法得到实例,则说明当前窗口第一次读取该环境变量:

  • 框架会创建一个新的可观察环境变量实例
  • 将该实例与当前窗口绑定
  • 完成初始化

初始化示例

我们通过一个例子来理解这个流程:

1import { uiObserver } from '@kit.ArkUI';
2
3@Entry
4@Component
5struct Index {
6  build() {
7    Column() {
8      Text(`Index`)
9      Child1()  // Child1会创建新的@Env实例
10      Child2()  // Child2的子组件会复用窗口中的实例
11    }
12    .height('100%')
13    .width('100%')
14  }
15}
16
17@Component
18struct Child1 {
19  // 第一次读取,会创建新实例并绑定到窗口
20  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
21
22  build() {
23    Column() {
24      Text(`Child1 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
25      Text(`Child1 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
26      GrandChild1()  // GrandChild1会复用Child1的实例
27    }
28  }
29}
30
31@Component
32struct Child2 {
33  build() {
34    Column() {
35      GrandChild2()  // GrandChild2会复用窗口中的实例
36    }
37  }
38}
39
40@Component
41struct GrandChild1 {
42  // 向上查找父组件,找到Child1的实例,直接复用
43  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
44
45  build() {
46    Column() {
47      Text(`GrandChild1 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
48      Text(`GrandChild1 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
49    }
50  }
51}
52
53@Component
54struct GrandChild2 {
55  // 向上查找父组件,没找到
56  // 查找当前窗口,找到Child1创建的实例,复用
57  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
58
59  build() {
60    Column() {
61      Text(`GrandChild2 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
62      Text(`GrandChild2 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
63    }
64  }
65}
66

初始化流程总结:

  • Child1初始化:向上查找父组件Index,没有实例 → 查找当前窗口,没有实例 → 创建新实例并绑定到窗口
  • GrandChild1初始化:向上查找父组件Child1,找到实例 → 直接复用
  • GrandChild2初始化:向上查找父组件Child2和Index,没有实例 → 查找当前窗口,找到Child1创建的实例 → 复用

七、@Env的限制条件

使用@Env时需要注意以下限制条件,违反这些条件会导致编译时报错:

1. 只能在组件中使用

@Env仅支持在@Component@ComponentV2中使用,否则会有编译时报错:

1import { uiObserver } from '@kit.ArkUI';
2
3//  错误:不能在普通类中使用
4class Info {
5  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
6}
7
8//  正确:在组件中使用
9@Entry
10@Component
11struct Index {
12  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo; // 正确用法
13
14  build() {
15  }
16}
17

2. 只读属性,不允许初始化

@Env装饰的变量为只读属性,不允许开发者进行初始化或赋值操作:

1import { uiObserver } from '@kit.ArkUI';
2
3@Entry
4@Component
5struct Index {
6  //  错误:不能初始化
7  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo =
8    new uiObserver.WindowSizeLayoutBreakpointInfo(); // 编译时报错
9
10  build() {
11    Column() {
12      Text(`breakpoint height ${this.breakpoint.heightBreakpoint}`).fontSize(20)
13      Text(`breakpoint width ${this.breakpoint.widthBreakpoint}`).fontSize(20)
14      Button('change breakpoint').onClick(() => {
15        //  错误:不能赋值
16        this.breakpoint = new uiObserver.WindowSizeLayoutBreakpointInfo(); // 编译时报错
17      })
18    }
19  }
20}
21

3. 仅支持BREAK_POINT参数

@Env当前仅支持SystemProperties.BREAK_POINT参数。若使用不支持的参数,将触发编译时报错:

1import { uiObserver } from '@kit.ArkUI';
2
3@Entry
4@Component
5struct Index {
6  //  正确:使用BREAK_POINT
7  @Env(SystemProperties.BREAK_POINT) breakpoint1: uiObserver.WindowSizeLayoutBreakpointInfo;
8
9  //  错误:使用不支持的参数
10  @Env('unsupported_key') breakpoint2: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
11
12  build() {
13    Text(`breakpoint2 width: ${this.breakpoint2.widthBreakpoint} height: ${this.breakpoint2.heightBreakpoint}`)
14  }
15}
16

4. 类型限制

@Env装饰的变量类型仅能为uiObserver.WindowSizeLayoutBreakpointInfo类型:

1import { uiObserver } from '@kit.ArkUI';
2
3@Entry
4@Component
5struct Index {
6  //  正确:使用WindowSizeLayoutBreakpointInfo类型
7  @Env(SystemProperties.BREAK_POINT) breakpoint1: uiObserver.WindowSizeLayoutBreakpointInfo;
8
9  //  错误:类型不匹配
10  @Env(SystemProperties.BREAK_POINT) breakpoint2: string; // 编译时报错
11
12  build() {
13  }
14}
15

5. 不能与其他装饰器联用

@Env只能单独使用,不能和其他V1V2状态变量装饰器或@Require联用:

1//  正确:单独使用
2@Env(SystemProperties.BREAK_POINT) breakpoint1: uiObserver.WindowSizeLayoutBreakpointInfo;
3
4//  错误:不能和@State联用
5@State @Env(SystemProperties.BREAK_POINT) breakpoint2: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
6
7//  错误:不能和@Require联用
8@Require @Env(SystemProperties.BREAK_POINT) breakpoint3: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
9
10//  错误:不能和@Local联用
11@Local @Env(SystemProperties.BREAK_POINT) breakpoint4: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
12

八、高级场景:通过BuilderNode切换窗口

@Env用于展示@Component/@ComponentV2所在窗口的环境变量信息。当我们通过BuilderNode切换组件所在的窗口实例时,@Env会根据新的窗口获取对应的环境变量信息,并触发关联的UI组件刷新。

场景说明

在下面的示例中,我们演示了如何通过BuilderNode在不同窗口间切换,以及@Env的行为:

  1. 点击Button('add node to tree'),创建BuilderNode节点挂载到NodeContainer
  2. 点击Button('remove node from tree'),将BuilderNode节点从NodeContainer上移除
  3. 点击Button('create sub window'),创建子窗并显示SubWindow窗口
  4. 点击SubWindow窗口内的Button('add node to tree'),将BuilderNode节点重新挂载到SubWindow内的NodeContainer

ComponentUnderBuilderNode被挂载到新的窗口下时,会触发@Env重新获取新的环境变量。

完整示例代码

1// EntryAbility.ets
2import { UIAbility } from '@kit.AbilityKit';
3import { hilog } from '@kit.PerformanceAnalysisKit';
4import { window } from '@kit.ArkUI';
5
6const DOMAIN = 0x0000;
7
8export default class EntryAbility extends UIAbility {
9  onWindowStageCreate(windowStage: window.WindowStage) {
10    windowStage.loadContent('pages/Index', (err) => {
11      if (err.code) {
12        hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
13        return;
14      }
15      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
16    })
17
18    // 给Index页面传递windowStage
19    AppStorage.setOrCreate('windowStage', windowStage);
20  }
21}
22
23// Index.ets
24import { BuilderNode, FrameNode, NodeController, uiObserver, window } from '@kit.ArkUI';
25import { BusinessError } from '@kit.BasicServicesKit';
26import { hilog } from '@kit.PerformanceAnalysisKit';
27
28const DOMAIN = 0x0000;
29
30let windowStage_: window.WindowStage | undefined = undefined;
31let sub_windowClass: window.Window | undefined = undefined;
32let globalBuilderNode: BuilderNode<[]> | undefined = undefined;
33
34// NodeController用于管理BuilderNode
35export class MyNodeController extends NodeController {
36  private rootNode: FrameNode | null = null;
37  private uiContext: UIContext | null = null;
38
39  makeNode(uiContext: UIContext): FrameNode | null {
40    this.rootNode = new FrameNode(uiContext);
41    this.uiContext = uiContext;
42    return this.rootNode;
43  }
44
45  // 添加BuilderNode到树中
46  addBuilderNode(): void {
47    if (!globalBuilderNode && this.uiContext) {
48      globalBuilderNode = new BuilderNode(this.uiContext);
49      globalBuilderNode.build(wrapBuilder<[]>(buildComponent), undefined);
50    }
51    if (this.rootNode && globalBuilderNode) {
52      this.rootNode.appendChild(globalBuilderNode.getFrameNode());
53    }
54  }
55
56  // 从树中移除BuilderNode
57  removeBuilderNode(): void {
58    if (this.rootNode && globalBuilderNode) {
59      this.rootNode.removeChild(globalBuilderNode.getFrameNode());
60    }
61  }
62
63  // 销毁BuilderNode
64  disposeNode(): void {
65    if (this.rootNode && globalBuilderNode) {
66      globalBuilderNode.dispose();
67      globalBuilderNode = undefined;
68    }
69  }
70}
71
72// Builder函数,构建要挂载的组件
73@Builder
74function buildComponent() {
75  Column() {
76    ComponentUnderBuilderNode()
77  }
78}
79
80@Entry
81@ComponentV2
82struct Index {
83  private nodeController: MyNodeController = new MyNodeController();
84
85  // 创建子窗口
86  private createSubWindow() {
87    windowStage_ = AppStorage.get('windowStage');
88    if (windowStage_ == null) {
89      hilog.error(DOMAIN, 'testTag', 'Failed to create the subwindow. Cause: windowStage_ is null');
90    } else {
91      // 创建应用子窗口
92      windowStage_.createSubWindow('mySubWindow', (err: BusinessError, data) => {
93        let errCode: number = err.code;
94        if (errCode) {
95          hilog.error(DOMAIN, 'testTag', 'Failed to create the subwindow. Cause: ' + JSON.stringify(err));
96          return;
97        }
98        sub_windowClass = data;
99        if (!sub_windowClass) {
100          hilog.error(DOMAIN, 'testTag', 'sub_windowClass is null');
101          return;
102        }
103        hilog.info(DOMAIN, 'testTag', 'Succeeded in creating the subwindow. Data: ' + JSON.stringify(data));
104        
105        // 子窗口创建成功后,设置子窗口的位置、大小及相关属性等
106        sub_windowClass.moveWindowTo(200, 1300, (err: BusinessError) => {
107          let errCode: number = err.code;
108          if (errCode) {
109            hilog.error(DOMAIN, 'testTag', 'Failed to move the window. Cause:' + JSON.stringify(err));
110            return;
111          }
112          hilog.info(DOMAIN, 'testTag', 'Succeeded in moving the window.');
113        });
114        
115        sub_windowClass.resize(900, 1800, (err: BusinessError) => {
116          let errCode: number = err.code;
117          if (errCode) {
118            hilog.error(DOMAIN, 'testTag', 'Failed to change the window size. Cause:' + JSON.stringify(err));
119            return;
120          }
121          hilog.info(DOMAIN, 'testTag', 'Succeeded in changing the window size.');
122        });
123        
124        // 为子窗口加载对应的目标页面
125        sub_windowClass.setUIContent('pages/SubWindow', (err: BusinessError) => {
126          let errCode: number = err.code;
127          if (errCode) {
128            hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause:' + JSON.stringify(err));
129            return;
130          }
131          hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
132          if (!sub_windowClass) {
133            hilog.error(DOMAIN, 'testTag', 'sub_windowClass is null');
134            return;
135          }
136          sub_windowClass.showWindow((err: BusinessError) => {
137            let errCode: number = err.code;
138            if (errCode) {
139              hilog.error(DOMAIN, 'testTag', 'Failed to show the window. Cause: ' + JSON.stringify(err));
140              return;
141            }
142            hilog.info(DOMAIN, 'testTag', 'Succeeded in showing the window.');
143          });
144        });
145      })
146    }
147  }
148
149  // 销毁子窗口
150  private destroySubWindow() {
151    if (!sub_windowClass) {
152      console.error('sub_windowClass is null');
153      return;
154    }
155    sub_windowClass.destroyWindow((err: BusinessError) => {
156      let errCode: number = err.code;
157      if (errCode) {
158        console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
159        return;
160      }
161      console.info('Succeeded in destroying the window.');
162    });
163  }
164
165  build() {
166    Column({ space: 10 }) {
167      Text(`Index`)
168      // 第一步:创建globalBuilderNode,并将globalBuilderNode下的节点挂在NodeContainer的占位节点下
169      Button('add node to tree').width(200).onClick(() => {
170        this.nodeController.addBuilderNode();
171      })
172      // 第二步:从NodeContainer的占位节点下移除globalBuilderNode下的节点
173      Button('remove node from tree').width(200).onClick(() => {
174        this.nodeController.removeBuilderNode();
175      })
176      // 销毁globalBuilderNode下的节点
177      Button('dispose node').width(200).onClick(() => {
178        this.nodeController.disposeNode();
179      })
180      // 第三步:创建子窗
181      Button([`create sub window`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.sub.md)).width(200).onClick(() => {
182        this.createSubWindow();
183      })
184      // 销毁子窗
185      Button([`destroy sub window`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.sub.md)).width(200).onClick(() => {
186        this.destroySubWindow();
187      })
188      NodeContainer(this.nodeController).backgroundColor('#FFEEF0')
189    }
190    .width('100%')
191    .height('100%')
192  }
193}
194
195// 在BuilderNode下的组件,使用@Env
196@Component
197struct ComponentUnderBuilderNode {
198  // @Env会根据组件所在的窗口获取环境变量
199  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
200
201  build() {
202    Column() {
203      Text(`ComponentUnderBuilderNode breakpoint width: ${this.breakpoint.widthBreakpoint}`)
204      Text(`ComponentUnderBuilderNode breakpoint height: ${this.breakpoint.heightBreakpoint}`)
205
206      // 传递给ComponentV2子组件
207      CompV2({ breakpoint: this.breakpoint })
208      // 传递给Component子组件(注意:在窗口切换场景下可能有问题)
209      Comp({ breakpoint: this.breakpoint })
210    }
211  }
212}
213
214@ComponentV2
215struct CompV2 {
216  @Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
217
218  build() {
219    Column() {
220      Text(`CompV2 breakpoint width: ${this.breakpoint.widthBreakpoint}`)
221      Text(`CompV2 breakpoint height: ${this.breakpoint.heightBreakpoint}`)
222    }
223  }
224}
225
226@Component
227struct Comp {
228  @Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
229
230  build() {
231    Column() {
232      Text(`Comp breakpoint width: ${this.breakpoint.widthBreakpoint}`)
233      Text(`Comp breakpoint height: ${this.breakpoint.heightBreakpoint}`)
234    }
235  }
236}
237
238// SubWindow.ets
239import { MyNodeController } from './Index';
240
241@Entry
242@Component
243struct SubWindow {
244  private nodeController: MyNodeController = new MyNodeController();
245
246  build() {
247    Column({ space: 10 }) {
248      Text(`SubWindow`)
249      // 第四步:在第一步中已在创建globalBuilderNode。将globalBuilderNode下的节点挂子窗的NodeContainer的占位节点下
250      Button('add node to tree').width(200).onClick(() => {
251        this.nodeController.addBuilderNode();
252      })
253      Button('remove node from tree').width(200).onClick(() => {
254        this.nodeController.removeBuilderNode();
255      })
256      Button('dispose node').width(200).onClick(() => {
257        this.nodeController.disposeNode();
258      })
259      NodeContainer(this.nodeController).backgroundColor('#FFEEF0')
260    }
261    .height('100%')
262    .width('100%')
263    .backgroundColor('#0D9FFB')
264  }
265}
266

重要提示

在切换窗口的场景中,@Env重新获取新的环境变量后,会触发其关联组件的刷新。但是需要注意:

  • ComponentUnderBuilderNode@Env(SystemProperties.BREAK_POINT) breakpoint会通知CompV2内的@Param breakpoint刷新
  • 但是并不会通知Comp内的常规变量breakpoint触发UI刷新

所以在切换窗口、@Env重新获取环境变量的场景下,建议开发者不要将@Env传递给常规变量,以避免常规变量不能被通知UI刷新的问题。

解决方案:使用lambda闭包函数

可以使用lambda闭包函数将ComponentUnderBuilderNode中的@Env向下传递。通过这种方式,@Env可以收集到子组件Comp内组件的依赖,在切换窗口实例的时候触发Comp内组件的刷新。

具体示例如下:

1@Component
2struct ComponentUnderBuilderNode {
3  @Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
4
5  build() {
6    Column() {
7      Text(`ComponentUnderBuilderNode breakpoint width: ${this.breakpoint.widthBreakpoint}`)
8      Text(`ComponentUnderBuilderNode breakpoint height: ${this.breakpoint.heightBreakpoint}`)
9
10      CompV2({ breakpoint: this.breakpoint })
11      // 通过lambda闭包函数,使得@Env可以关联到Comp内的组件
12      Comp({ getEnv: () => this.breakpoint })
13    }
14  }
15}
16
17@ComponentV2
18struct CompV2 {
19  @Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
20
21  build() {
22    Column() {
23      Text(`CompV2 breakpoint width: ${this.breakpoint.widthBreakpoint}`)
24      Text(`CompV2 breakpoint height: ${this.breakpoint.heightBreakpoint}`)
25    }
26  }
27}
28
29@Component
30struct Comp {
31  // 通过lambda闭包函数获取父组件的@Env的实例
32  @Require getEnv: () => uiObserver.WindowSizeLayoutBreakpointInfo;
33
34  build() {
35    Column() {
36      // 调用闭包函数获取最新的环境变量
37      Text(`Comp breakpoint width: ${this.getEnv().widthBreakpoint}`)
38      Text(`Comp breakpoint height: ${this.getEnv().heightBreakpoint}`)
39    }
40  }
41}
42

通过lambda闭包函数的方式,Comp组件可以正确响应@Env的变化,即使在窗口切换的场景下也能正常工作。

九、结尾/总结

好了,关于@Env的内容我们就介绍到这里。总结一下:

@Env是API 22引入的响应式系统环境变量装饰器,它的核心价值在于:

  1. 响应式能力:系统环境变量变化时自动触发UI刷新,无需手动管理
  2. 简化代码:减少了大量重复的适配逻辑和监听代码
  3. 多设备适配:特别适合多设备开发场景,尤其是响应式布局和横竖屏适配

虽然@Env目前只支持BREAK_POINT参数(这不是虽然了,这是很遗憾。),但在窗口断点相关的场景中,它比Environment更加方便和强大。如果你需要其他环境变量(如语言、主题等),还是需要使用Environment

总的来说,@Env是一个很好的补充,让我们的开发变得更加简单,少写点,稍微维护点代码,虽然现在可用性不高。随着API版本的迭代,@Env会支持更多的环境变量参数,到时候它的价值会更加明显(越快越好)。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

十、感谢

各位读者老爷


HarmonyOS一杯冰美式的时间 -- @Env》 是转载文章,点击查看原文


相关推荐


Nop入门:使用Excel模板生成包含图表的报表
canonical_entropy2025/12/30

讲解视频:Nop入门:使用NopReport导出Excel图表_哔哩哔哩_bilibili Nop平台内置了一个非常精简的中国式报表引擎NopReport,它可以实现商业报表引擎如FineReport和润乾报表的核心功能。NopReport很巧妙的利用Excel单元格的批注机制来存放扩展信息,可以直接使用Excel模板来作为报表模板,这样大大简化了报表制作过程,并且可以复用客户已有的各种业务表格。 在实现层面,NopReport没有使用Apache POI库,而是选择了直接使用流式接口解析Off


什么?还不知道git cherry pick?
少年姜太公2025/12/21

前言 上周四在公司开发代码时,一下没注意从测试分支dev上拉了代码并进行开发,最后要上线的时候才猛然发现分支拉错了,一看gitLab上改了几十个文件,再一查发现自己拉代码拉错了😭😭😭,本来都准备下班了,结果硬是新建了一个分支,把四五个文件近1000行代码一点一点cv到新分支上,结果搞完一查才发现git有个cherry pick的命令可以直接搞完,希望jym看完这篇文章以后不要像我一样做这种一点一点cv的蠢事了😭😭😭 什么是cherry pick cherry pick顾名思义,就是樱


【鸿蒙开发案例篇】拒绝裸奔!鸿蒙6实现PDF动态加密
威哥爱编程2025/12/13

兄弟们抄起键盘!今天V哥要带大家用鸿蒙6.0的pdfService玩转PDF动态加密,让敏感文档在战场上穿隐身衣。以下基于HarmonyOS 6.0(API 21)的ArkTS实战,全程高能代码爆破,专治数据泄露不服!💣 联系V哥获取 鸿蒙学习资料 🔑 第一弹:动态加密核心战备(理论基础) 作战目标:运行时根据设备状态动态加载/更新PDF加密密钥技术依据: pdfService支持通过setEncryptConfig()对文档进行AES-256加密 加密状态可通过getSecurityH


TRAE SOLO 驱动:重构AI模拟面试产品的复盘
HiStewie2025/12/4

面试是一项技能,而任何技能的进阶都离不开“高频练习”与“即时反馈”这两个核心要素。 传统的面试准备往往陷入两个极端:要么对着镜子自言自语,缺乏客观的第三方视角;要么花重金找导师模拟,成本高昂且难以常态化。技术存在的意义,就是用低边际成本解决高频痛点。 但这其实并不是我第一次尝试解决这个问题。在此之前,我曾开发过一个初版工具:(QuizPort1.0 让每篇好文都有测验陪跑) 当时的思路很线性:抓取技术文章 →\rightarrow→ AI 提炼知识点 →\rightarrow→ 生成文字面试题


Django 踩坑记:OceanBase 4012 Timeout 两条红线,语句超时 vs 事务超时一次讲透
哈里谢顿2026/1/16

一、4012 是谁抛的? Django 本身没有 4012 错误码,它是 OceanBase 的“杀手”信号: 当前 SQL 或 当前事务累计执行时间 ≥ 系统阈值,直接返回 4012。 二、两条红线长啥样? 变量名默认阈值计时对象触发后果ob_query_timeout10 000 000 µs = 10 s单条 SQL 执行时长这条语句被杀,事务可继续ob_trx_timeout100 000 000 µs = 100 s事务 begin→


VS code 类产物中 win11 终端字体内容和颜色 加粗不匹配问题
小兵张健2026/1/24

我尝试了各种方式,换字体,改配置,结果还是乱的,有人知道怎么搞吗?跪求,Mac好像天然就没问题,急急急大佬们

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客