一、前言
该系列依旧会带着大家,了解,开阔一些不怎么热门的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的行为:
- 点击
Button('add node to tree'),创建BuilderNode节点挂载到NodeContainer下 - 点击
Button('remove node from tree'),将BuilderNode节点从NodeContainer上移除 - 点击
Button('create sub window'),创建子窗并显示SubWindow窗口 - 点击
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引入的响应式系统环境变量装饰器,它的核心价值在于:
- 响应式能力:系统环境变量变化时自动触发UI刷新,无需手动管理
- 简化代码:减少了大量重复的适配逻辑和监听代码
- 多设备适配:特别适合多设备开发场景,尤其是响应式布局和横竖屏适配
虽然@Env目前只支持BREAK_POINT参数(这不是虽然了,这是很遗憾。),但在窗口断点相关的场景中,它比Environment更加方便和强大。如果你需要其他环境变量(如语言、主题等),还是需要使用Environment。
总的来说,@Env是一个很好的补充,让我们的开发变得更加简单,少写点,稍微维护点代码,虽然现在可用性不高。随着API版本的迭代,@Env会支持更多的环境变量参数,到时候它的价值会更加明显(越快越好)。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
十、感谢
各位读者老爷
《HarmonyOS一杯冰美式的时间 -- @Env》 是转载文章,点击查看原文。