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

58 阅读14分钟

一、前言

该系列依旧会带着大家,了解,开阔一些不怎么热门的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的行为:

  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重新获取新的环境变量。

完整示例代码

// 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引入的响应式系统环境变量装饰器,它的核心价值在于:

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

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

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

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

十、感谢

各位读者老爷