一、前言
该系列依旧会带着大家,了解,开阔一些不怎么热门的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,用于获取窗口不同宽高阈值下对应的断点值信息。
import { uiObserver } from '@kit.ArkUI';
@Entry
@Component
struct Index {
// 使用@Env装饰器获取窗口断点信息
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
// 可以直接使用breakpoint获取宽度和高度断点
Text(`宽度断点: ${this.breakpoint.widthBreakpoint}`)
Text(`高度断点: ${this.breakpoint.heightBreakpoint}`)
}
}
}
(2)响应式更新
当系统环境变量改变时(比如横竖屏切换、窗口大小调整),@Env会自动:
- 通知
@Env装饰变量的更新 - 触发
@Env关联组件的刷新 - 实现界面内容的同步更新
不需要手动监听变化,框架会自动处理。
(3)可观察对象
@Env返回的对象实际上是由@ObservedV2装饰的可观察对象,其属性由@Trace装饰。这意味着:
- 你可以使用
addMonitor来监听属性的变化 - 属性的变化会自动触发UI更新
- 支持细粒度的响应式更新
五、@Env基本使用
1. 在@ComponentV2中使用@Env
在@ComponentV2中使用@Env非常简单,我们来看一个完整的例子:
import { uiObserver, UIUtils, window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
@Entry
@ComponentV2
struct Index {
// 声明@Env变量,获取窗口断点信息
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
// 切换横竖屏的方法
private changeOrientation(isLandscape: boolean) {
const context = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
window.getLastWindow(context).then((lastWindow) => {
// 设置窗口方向
lastWindow.setPreferredOrientation(
isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT
);
});
}
// 监听断点变化的回调
orientationChange(mon: IMonitor) {
mon.dirty.forEach((path: string) => {
console.info(`${path} changes from ${mon.value(path)?.before} to ${mon.value(path)?.now}`);
})
}
aboutToAppear(): void {
// @Env返回的对象实际上是@ObservedV2装饰的对象(其属性是@Trace装饰的)
// 所以其属性的改变可以通过addMonitor监听
UIUtils.addMonitor(
this.breakpoint,
['widthBreakpoint', 'heightBreakpoint'],
this.orientationChange
);
}
build() {
Column() {
// 显示当前断点信息
Text(`Index breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`Index breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
// 横屏按钮
Button('Landscape').onClick(() => {
this.changeOrientation(true);
})
// 竖屏按钮
Button('Portrait').onClick(() => {
this.changeOrientation(false);
})
// 将@Env变量传递给子组件
CompV2({ breakpoint: this.breakpoint })
Comp({ breakpoint: this.breakpoint })
}
}
}
// ComponentV2子组件
@ComponentV2
struct CompV2 {
// @Env装饰的变量只能用于初始化@Param装饰的变量
@Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`CompV2 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`CompV2 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
}
}
}
// Component子组件
@Component
struct Comp {
// @Env装饰的变量只能用于初始化常规变量
@Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`Comp breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`Comp breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
}
}
}
2. 在@Component中使用@Env
@Env在@Component中的使用方式和@ComponentV2类似,我们看看代码:
import { uiObserver, UIUtils, window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
// 在@Component中也可以使用@Env
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
private changeOrientation(isLandscape: boolean) {
const context = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
window.getLastWindow(context).then((lastWindow) => {
lastWindow.setPreferredOrientation(
isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT
);
});
}
orientationChange(mon: IMonitor) {
mon.dirty.forEach((path: string) => {
console.info(`${path} changes from ${mon.value(path)?.before} to ${mon.value(path)?.now}`);
})
}
aboutToAppear(): void {
// 同样可以使用addMonitor监听
UIUtils.addMonitor(
this.breakpoint,
['widthBreakpoint', 'heightBreakpoint'],
this.orientationChange
);
}
build() {
Column() {
Text(`Index breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`Index breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
Button('Landscape').onClick(() => {
this.changeOrientation(true);
})
Button('Portrait').onClick(() => {
this.changeOrientation(false);
})
CompV2({ breakpoint: this.breakpoint })
Comp({ breakpoint: this.breakpoint })
}
}
}
// 子组件使用方式相同
@ComponentV2
struct CompV2 {
@Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`CompV2 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`CompV2 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
}
}
}
@Component
struct Comp {
@Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`Comp breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`Comp breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
}
}
}
可以看到,@Component和@ComponentV2中使用@Env的方式基本一致,主要区别在于子组件接收参数的方式。
3. 变量传递规则
@Env装饰的变量在组件间传递有严格的规则,我们需要特别注意:
(1)传递给ComponentV2子组件
@Env装饰的变量只能用于初始化@ComponentV2中@Param装饰的变量:
@Entry
@Component
struct Index {
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
// 正确:传递给@Param
CompV2({ breakpoint: this.breakpoint })
// 错误:不能传递给非@Param变量
CompV2Invalid({ breakpoint: this.breakpoint })
}
}
}
@ComponentV2
struct CompV2 {
// 正确:使用@Require @Param接收
@Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
// ...
}
}
@ComponentV2
struct CompV2Invalid {
// 错误:缺少@Param
@Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
// ...
}
}
(2)传递给Component子组件
@Env装饰的变量只能用于初始化@Component中的常规变量:
@Entry
@Component
struct Index {
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
// 正确:传递给常规变量
Comp({ breakpoint: this.breakpoint })
// 错误:不能传递给@ObjectLink等
CompInvalid({ breakpoint: this.breakpoint })
}
}
}
@Component
struct Comp {
// 正确:使用@Require接收常规变量
@Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
// ...
}
}
@Component
struct CompInvalid {
// 错误:不能使用@ObjectLink
@ObjectLink breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
// ...
}
}
重要提示:通过BuilderNode切换窗口时,会导致@Env依据新的窗口更新环境变量实例。在切换窗口的场景中,不建议使用@Env变量来初始化子组件的常规变量,否则会造成该常规变量无法被@Env通知触发其关联UI组件刷新。具体解决方案我们会在高级场景中介绍。
六、@Env初始化流程详解
@Env变量不允许开发者初始化,其值由框架根据当前窗口的环境变量自动提供。@Env变量在被第一次读值的时候,会触发初始化。初始化遵循以下流程:
1. 从父组件中查找已有实例
框架会向上递归查找父组件:
- 如果某个父组件在同一窗口中已经初始化过相同key的
@Env变量,则直接复用该实例 - 若未找到,则继续向上查找,直到父组件为空
- 注意:向上查找父组件的流程会被
BuilderNode打断
2. 查找当前窗口的@Env实例
如果在父组件中未找到对应的实例,则检查当前窗口是否已有相同key的@Env变量实例:
- 如存在,则复用该窗口内的
@Env实例
3. 首次请求:创建新环境变量实例
若以上两步都无法得到实例,则说明当前窗口第一次读取该环境变量:
- 框架会创建一个新的可观察环境变量实例
- 将该实例与当前窗口绑定
- 完成初始化
初始化示例
我们通过一个例子来理解这个流程:
import { uiObserver } from '@kit.ArkUI';
@Entry
@Component
struct Index {
build() {
Column() {
Text(`Index`)
Child1() // Child1会创建新的@Env实例
Child2() // Child2的子组件会复用窗口中的实例
}
.height('100%')
.width('100%')
}
}
@Component
struct Child1 {
// 第一次读取,会创建新实例并绑定到窗口
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`Child1 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`Child1 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
GrandChild1() // GrandChild1会复用Child1的实例
}
}
}
@Component
struct Child2 {
build() {
Column() {
GrandChild2() // GrandChild2会复用窗口中的实例
}
}
}
@Component
struct GrandChild1 {
// 向上查找父组件,找到Child1的实例,直接复用
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`GrandChild1 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`GrandChild1 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
}
}
}
@Component
struct GrandChild2 {
// 向上查找父组件,没找到
// 查找当前窗口,找到Child1创建的实例,复用
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`GrandChild2 breakpoint width: ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Text(`GrandChild2 breakpoint height: ${this.breakpoint.heightBreakpoint}`).fontSize(20)
}
}
}
初始化流程总结:
- Child1初始化:向上查找父组件Index,没有实例 → 查找当前窗口,没有实例 → 创建新实例并绑定到窗口
- GrandChild1初始化:向上查找父组件Child1,找到实例 → 直接复用
- GrandChild2初始化:向上查找父组件Child2和Index,没有实例 → 查找当前窗口,找到Child1创建的实例 → 复用
七、@Env的限制条件
使用@Env时需要注意以下限制条件,违反这些条件会导致编译时报错:
1. 只能在组件中使用
@Env仅支持在@Component和@ComponentV2中使用,否则会有编译时报错:
import { uiObserver } from '@kit.ArkUI';
// 错误:不能在普通类中使用
class Info {
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
}
// 正确:在组件中使用
@Entry
@Component
struct Index {
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo; // 正确用法
build() {
}
}
2. 只读属性,不允许初始化
@Env装饰的变量为只读属性,不允许开发者进行初始化或赋值操作:
import { uiObserver } from '@kit.ArkUI';
@Entry
@Component
struct Index {
// 错误:不能初始化
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo =
new uiObserver.WindowSizeLayoutBreakpointInfo(); // 编译时报错
build() {
Column() {
Text(`breakpoint height ${this.breakpoint.heightBreakpoint}`).fontSize(20)
Text(`breakpoint width ${this.breakpoint.widthBreakpoint}`).fontSize(20)
Button('change breakpoint').onClick(() => {
// 错误:不能赋值
this.breakpoint = new uiObserver.WindowSizeLayoutBreakpointInfo(); // 编译时报错
})
}
}
}
3. 仅支持BREAK_POINT参数
@Env当前仅支持SystemProperties.BREAK_POINT参数。若使用不支持的参数,将触发编译时报错:
import { uiObserver } from '@kit.ArkUI';
@Entry
@Component
struct Index {
// 正确:使用BREAK_POINT
@Env(SystemProperties.BREAK_POINT) breakpoint1: uiObserver.WindowSizeLayoutBreakpointInfo;
// 错误:使用不支持的参数
@Env('unsupported_key') breakpoint2: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
build() {
Text(`breakpoint2 width: ${this.breakpoint2.widthBreakpoint} height: ${this.breakpoint2.heightBreakpoint}`)
}
}
4. 类型限制
@Env装饰的变量类型仅能为uiObserver.WindowSizeLayoutBreakpointInfo类型:
import { uiObserver } from '@kit.ArkUI';
@Entry
@Component
struct Index {
// 正确:使用WindowSizeLayoutBreakpointInfo类型
@Env(SystemProperties.BREAK_POINT) breakpoint1: uiObserver.WindowSizeLayoutBreakpointInfo;
// 错误:类型不匹配
@Env(SystemProperties.BREAK_POINT) breakpoint2: string; // 编译时报错
build() {
}
}
5. 不能与其他装饰器联用
@Env只能单独使用,不能和其他V1V2状态变量装饰器或@Require联用:
// 正确:单独使用
@Env(SystemProperties.BREAK_POINT) breakpoint1: uiObserver.WindowSizeLayoutBreakpointInfo;
// 错误:不能和@State联用
@State @Env(SystemProperties.BREAK_POINT) breakpoint2: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
// 错误:不能和@Require联用
@Require @Env(SystemProperties.BREAK_POINT) breakpoint3: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
// 错误:不能和@Local联用
@Local @Env(SystemProperties.BREAK_POINT) breakpoint4: uiObserver.WindowSizeLayoutBreakpointInfo; // 编译时报错
八、高级场景:通过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重新获取新的环境变量。
完整示例代码
// EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
})
// 给Index页面传递windowStage
AppStorage.setOrCreate('windowStage', windowStage);
}
}
// Index.ets
import { BuilderNode, FrameNode, NodeController, uiObserver, window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
let windowStage_: window.WindowStage | undefined = undefined;
let sub_windowClass: window.Window | undefined = undefined;
let globalBuilderNode: BuilderNode<[]> | undefined = undefined;
// NodeController用于管理BuilderNode
export class MyNodeController extends NodeController {
private rootNode: FrameNode | null = null;
private uiContext: UIContext | null = null;
makeNode(uiContext: UIContext): FrameNode | null {
this.rootNode = new FrameNode(uiContext);
this.uiContext = uiContext;
return this.rootNode;
}
// 添加BuilderNode到树中
addBuilderNode(): void {
if (!globalBuilderNode && this.uiContext) {
globalBuilderNode = new BuilderNode(this.uiContext);
globalBuilderNode.build(wrapBuilder<[]>(buildComponent), undefined);
}
if (this.rootNode && globalBuilderNode) {
this.rootNode.appendChild(globalBuilderNode.getFrameNode());
}
}
// 从树中移除BuilderNode
removeBuilderNode(): void {
if (this.rootNode && globalBuilderNode) {
this.rootNode.removeChild(globalBuilderNode.getFrameNode());
}
}
// 销毁BuilderNode
disposeNode(): void {
if (this.rootNode && globalBuilderNode) {
globalBuilderNode.dispose();
globalBuilderNode = undefined;
}
}
}
// Builder函数,构建要挂载的组件
@Builder
function buildComponent() {
Column() {
ComponentUnderBuilderNode()
}
}
@Entry
@ComponentV2
struct Index {
private nodeController: MyNodeController = new MyNodeController();
// 创建子窗口
private createSubWindow() {
windowStage_ = AppStorage.get('windowStage');
if (windowStage_ == null) {
hilog.error(DOMAIN, 'testTag', 'Failed to create the subwindow. Cause: windowStage_ is null');
} else {
// 创建应用子窗口
windowStage_.createSubWindow('mySubWindow', (err: BusinessError, data) => {
let errCode: number = err.code;
if (errCode) {
hilog.error(DOMAIN, 'testTag', 'Failed to create the subwindow. Cause: ' + JSON.stringify(err));
return;
}
sub_windowClass = data;
if (!sub_windowClass) {
hilog.error(DOMAIN, 'testTag', 'sub_windowClass is null');
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in creating the subwindow. Data: ' + JSON.stringify(data));
// 子窗口创建成功后,设置子窗口的位置、大小及相关属性等
sub_windowClass.moveWindowTo(200, 1300, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
hilog.error(DOMAIN, 'testTag', 'Failed to move the window. Cause:' + JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in moving the window.');
});
sub_windowClass.resize(900, 1800, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
hilog.error(DOMAIN, 'testTag', 'Failed to change the window size. Cause:' + JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in changing the window size.');
});
// 为子窗口加载对应的目标页面
sub_windowClass.setUIContent('pages/SubWindow', (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause:' + JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
if (!sub_windowClass) {
hilog.error(DOMAIN, 'testTag', 'sub_windowClass is null');
return;
}
sub_windowClass.showWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
hilog.error(DOMAIN, 'testTag', 'Failed to show the window. Cause: ' + JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in showing the window.');
});
});
})
}
}
// 销毁子窗口
private destroySubWindow() {
if (!sub_windowClass) {
console.error('sub_windowClass is null');
return;
}
sub_windowClass.destroyWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in destroying the window.');
});
}
build() {
Column({ space: 10 }) {
Text(`Index`)
// 第一步:创建globalBuilderNode,并将globalBuilderNode下的节点挂在NodeContainer的占位节点下
Button('add node to tree').width(200).onClick(() => {
this.nodeController.addBuilderNode();
})
// 第二步:从NodeContainer的占位节点下移除globalBuilderNode下的节点
Button('remove node from tree').width(200).onClick(() => {
this.nodeController.removeBuilderNode();
})
// 销毁globalBuilderNode下的节点
Button('dispose node').width(200).onClick(() => {
this.nodeController.disposeNode();
})
// 第三步:创建子窗
Button(`create sub window`).width(200).onClick(() => {
this.createSubWindow();
})
// 销毁子窗
Button(`destroy sub window`).width(200).onClick(() => {
this.destroySubWindow();
})
NodeContainer(this.nodeController).backgroundColor('#FFEEF0')
}
.width('100%')
.height('100%')
}
}
// 在BuilderNode下的组件,使用@Env
@Component
struct ComponentUnderBuilderNode {
// @Env会根据组件所在的窗口获取环境变量
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`ComponentUnderBuilderNode breakpoint width: ${this.breakpoint.widthBreakpoint}`)
Text(`ComponentUnderBuilderNode breakpoint height: ${this.breakpoint.heightBreakpoint}`)
// 传递给ComponentV2子组件
CompV2({ breakpoint: this.breakpoint })
// 传递给Component子组件(注意:在窗口切换场景下可能有问题)
Comp({ breakpoint: this.breakpoint })
}
}
}
@ComponentV2
struct CompV2 {
@Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`CompV2 breakpoint width: ${this.breakpoint.widthBreakpoint}`)
Text(`CompV2 breakpoint height: ${this.breakpoint.heightBreakpoint}`)
}
}
}
@Component
struct Comp {
@Require breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`Comp breakpoint width: ${this.breakpoint.widthBreakpoint}`)
Text(`Comp breakpoint height: ${this.breakpoint.heightBreakpoint}`)
}
}
}
// SubWindow.ets
import { MyNodeController } from './Index';
@Entry
@Component
struct SubWindow {
private nodeController: MyNodeController = new MyNodeController();
build() {
Column({ space: 10 }) {
Text(`SubWindow`)
// 第四步:在第一步中已在创建globalBuilderNode。将globalBuilderNode下的节点挂子窗的NodeContainer的占位节点下
Button('add node to tree').width(200).onClick(() => {
this.nodeController.addBuilderNode();
})
Button('remove node from tree').width(200).onClick(() => {
this.nodeController.removeBuilderNode();
})
Button('dispose node').width(200).onClick(() => {
this.nodeController.disposeNode();
})
NodeContainer(this.nodeController).backgroundColor('#FFEEF0')
}
.height('100%')
.width('100%')
.backgroundColor('#0D9FFB')
}
}
重要提示
在切换窗口的场景中,@Env重新获取新的环境变量后,会触发其关联组件的刷新。但是需要注意:
ComponentUnderBuilderNode中@Env(SystemProperties.BREAK_POINT) breakpoint会通知CompV2内的@Param breakpoint刷新- 但是并不会通知
Comp内的常规变量breakpoint触发UI刷新
所以在切换窗口、@Env重新获取环境变量的场景下,建议开发者不要将@Env传递给常规变量,以避免常规变量不能被通知UI刷新的问题。
解决方案:使用lambda闭包函数
可以使用lambda闭包函数将ComponentUnderBuilderNode中的@Env向下传递。通过这种方式,@Env可以收集到子组件Comp内组件的依赖,在切换窗口实例的时候触发Comp内组件的刷新。
具体示例如下:
@Component
struct ComponentUnderBuilderNode {
@Env(SystemProperties.BREAK_POINT) breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`ComponentUnderBuilderNode breakpoint width: ${this.breakpoint.widthBreakpoint}`)
Text(`ComponentUnderBuilderNode breakpoint height: ${this.breakpoint.heightBreakpoint}`)
CompV2({ breakpoint: this.breakpoint })
// 通过lambda闭包函数,使得@Env可以关联到Comp内的组件
Comp({ getEnv: () => this.breakpoint })
}
}
}
@ComponentV2
struct CompV2 {
@Require @Param breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
Text(`CompV2 breakpoint width: ${this.breakpoint.widthBreakpoint}`)
Text(`CompV2 breakpoint height: ${this.breakpoint.heightBreakpoint}`)
}
}
}
@Component
struct Comp {
// 通过lambda闭包函数获取父组件的@Env的实例
@Require getEnv: () => uiObserver.WindowSizeLayoutBreakpointInfo;
build() {
Column() {
// 调用闭包函数获取最新的环境变量
Text(`Comp breakpoint width: ${this.getEnv().widthBreakpoint}`)
Text(`Comp breakpoint height: ${this.getEnv().heightBreakpoint}`)
}
}
}
通过lambda闭包函数的方式,Comp组件可以正确响应@Env的变化,即使在窗口切换的场景下也能正常工作。
九、结尾/总结
好了,关于@Env的内容我们就介绍到这里。总结一下:
@Env是API 22引入的响应式系统环境变量装饰器,它的核心价值在于:
- 响应式能力:系统环境变量变化时自动触发UI刷新,无需手动管理
- 简化代码:减少了大量重复的适配逻辑和监听代码
- 多设备适配:特别适合多设备开发场景,尤其是响应式布局和横竖屏适配
虽然@Env目前只支持BREAK_POINT参数(这不是虽然了,这是很遗憾。),但在窗口断点相关的场景中,它比Environment更加方便和强大。如果你需要其他环境变量(如语言、主题等),还是需要使用Environment。
总的来说,@Env是一个很好的补充,让我们的开发变得更加简单,少写点,稍微维护点代码,虽然现在可用性不高。随着API版本的迭代,@Env会支持更多的环境变量参数,到时候它的价值会更加明显(越快越好)。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
十、感谢
各位读者老爷